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() # endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE) 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() else()
add_definitions(-Wall -Wextra) 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... ") 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")) run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable: if python_executable:
+1 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif endif
# Current build commit hash. # Current build commit hash.
OLD:=03935a9 OLD:=f5e02d1
.PHONY: bump .PHONY: bump
bump: bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
@@ -183,10 +183,10 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["predefined_types"] = data blender_messages[sid]["predefined_types"] = data
await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web") 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}") print(f"Selected products from Blender client {sid}")
blender_messages[sid]["selected_products"] = data blender_messages[sid]["quantities"] = data
await sio.emit("selected_products", {"blenderId": sid, "data": data}, namespace="/web") await sio.emit("quantities", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request): async def schedules(request):
with open("templates/index.html", "r") as f: with open("templates/index.html", "r") as f:
@@ -20,6 +20,22 @@ body {
box-sizing: border-box; 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 { .form-header {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@@ -30,37 +46,44 @@ body {
font-size: large; font-size: large;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
border-radius: 6px 6px 0 0; border-radius: 6px 6px 0 0;
height: 5%;
} }
.floating-form span { .form-container {
padding: var(--padding-small); height: 95%;
margin-left: var(--margin-small); padding-left: var(--padding-medium);
padding-right: var(--padding-medium);
} }
.floating-form { .form-section {
width: 50vw; margin-bottom: 1em;
max-height: 80vh; height: 65%;
position: absolute; overflow-y: auto;
background-color: var(--background-color); overflow-x: hidden;
border: 1px solid black;
z-index: 9999;
cursor: move;
user-select: none;
resize: both;
} }
.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 { .floating-form table tr {
margin-bottom: 10px; margin-bottom: 10px;
} }
.form-container {
padding: var(--padding-medium);
overflow-y: auto;
overflow-x: hidden;
}
.action-button { .action-button {
position:relative;
background-color: var(--button-background); background-color: var(--button-background);
border: none; border: none;
color: #fff; color: #fff;
@@ -73,6 +96,32 @@ body {
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
width: fit-content; width: fit-content;
margin: 0.5em; 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 { .action-button:hover {
@@ -142,7 +191,7 @@ body {
} }
.floating-form .table-container { .floating-form .table-container {
max-height: 70%; max-height: 80%;
} }
#cost-items { #cost-items {
@@ -150,12 +199,10 @@ body {
} }
[id^="cost-values-form"] table { [id^="cost-values-form"] table {
/* fixed */
max-height: 50%; max-height: 50%;
table-layout: fixed; table-layout: fixed;
} }
[id^="selected-products"] .form-container { [id^="selected-products"] .form-container {
height: 50vh; height: 50vh;
} }
@@ -186,9 +233,9 @@ tbody {
td, td,
th { th {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
text-align: center; text-align: center;
height: 100%;
} }
th { th {
@@ -224,15 +271,7 @@ select {
color: var(--blender-button-text, var(--text-color)); color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color)); border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease; 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); 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 { input:focus {
@@ -242,6 +281,31 @@ input:focus {
color: #ddd; 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, #cost-items tr:hover,
.highlighted { .highlighted {
background-color: #28a74657; background-color: #28a74657;
@@ -304,8 +368,38 @@ form table {
max-height: 50%; max-height: 50%;
} }
.subtotal-row { .subtotal-row {
background-color: var(--button-hover-background); background-color: var(--button-hover-background);
font-weight: bold; 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_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData); socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData); socket.on("cost_value", handleCostValueData);
socket.on("selected_products", handleSelectedProducts); socket.on("quantities", handleEditQuantities);
} }
function handleSelectedProducts(data) { function handleEditQuantities(data) {
const costItemId = data.data["selected_products"]["cost_item_id"]; const costItemId = data.data["quantities"]["cost_item_id"];
const products = data.data["selected_products"]["selected_products"]; const products = data.data["quantities"]["selected_products"];
const assigned_products = data.data["selected_products"]["assigned_products"]; const assigned_products = data.data["quantities"]["assigned_products"];
const quantityNames = const quantityNames = data.data["quantities"]["product_quantity_names"];
data.data["selected_products"]["product_quantity_names"]; const costQuantities = data.data["quantities"]["cost_quantities"];
const formId = "selected-products-" + costItemId;
const form = CostUI.Form({ CostUI.enableEditingQuantities({
id: formId, costItemId: costItemId,
name: "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId), selectedProducts: products,
icon: "fa-solid fa-box", assignedProducts: assigned_products,
}); quantityNames: quantityNames,
const numberOfProducts = CostUI.Text( costQuantities: costQuantities,
"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,
callbacks: { callbacks: {
addProductAssignments: addProductAssignments, addProductAssignments: addProductAssignments,
getSelectedProducts: getSelectedProducts, enableEditingQuantities: enableEditingQuantities,
addQuantity: addQuantity,
editQuantity: editQuantity,
deleteQuantity: deleteQuantity,
}, },
}); });
}
if (assigned_products.length > 0) { function addQuantity(costItemId, ifcClass) {
const assignedProductsText = CostUI.Text( executeOperator({
"Assigned Products", type: "AddCostItemQuantity",
"fa-solid fa-solid fa-paperclip", costItemId: costItemId,
"large" ifcClass: ifcClass,
); });
form.appendChild(assignedProductsText);
const assignmentsTable = CostUI.createAssignmentsTable({ //CostUI.addQuantity(costItemId, quantityName);
form, }
products: assigned_products,
quantityNames, function editQuantity(costItemId, quantityId, attributes) {
costItemId, executeOperator({
}); type: "editCostItemQuantity",
} costItemId: costItemId,
quantityId: quantityId,
attributes: attributes,
});
}
function deleteQuantity(costItemId, quantityId) {
executeOperator({
type: "deleteCostItemQuantity",
costItemId: costItemId,
quantityId: quantityId,
});
} }
function handlePredefinedTypes(data) { function handlePredefinedTypes(data) {
@@ -322,9 +326,6 @@ function handleCostSchedulesData(data) {
if (costSchedules.length === 0) { if (costSchedules.length === 0) {
return; return;
} }
const currency = data.data["cost_schedules"]["currency"]
? data.data["cost_schedules"]["currency"]["name"]
: "Undefined";
const costScheduleDiv = document.getElementById("cost-schedules"); const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = ""; costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => { costSchedules.forEach((costSchedule) => {
@@ -368,8 +369,13 @@ function handleCostItemsData(data) {
loadedSchedules[data.blenderId] = costScheduleId; loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId); CostUI.highlightElement("schedule-" + costScheduleId);
const currency = data.data["cost_items"]["currency"]
? data.data["cost_items"]["currency"]["name"]
: "Undefined";
CostUI.createCostSchedule({ CostUI.createCostSchedule({
data: data.data["cost_items"]["cost_items"], costItems: data.data["cost_items"]["cost_items"],
currency: currency,
costScheduleId: costScheduleId, costScheduleId: costScheduleId,
blenderID: data.blenderId, blenderID: data.blenderId,
callbacks: { callbacks: {
@@ -380,13 +386,13 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName, editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues, enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem, addSummaryCostItem: addSummaryCostItem,
getSelectedProducts: getSelectedProducts, enableEditingQuantities: enableEditingQuantities,
}, },
}); });
} }
function getSelectedProducts(costItemId) { function enableEditingQuantities(costItemId) {
executeOperator({ type: "getSelectedProducts", costItemId: costItemId }); executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
} }
function duplicateCostItem(costItemId) { function duplicateCostItem(costItemId) {
@@ -9,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) { static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove(); document.getElementById("cost-items-" + id).remove();
} }
static createTable(id, callbacks) { static createCostTable(id, currency, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table"); const table = document.createElement("table");
@@ -21,9 +21,9 @@ export class CostUI {
"Name", "Name",
"Quantity", "Quantity",
"Unit", "Unit",
"Cost", "Cost (" + currency + ")",
"Total Cost", "Total Cost (" + currency + ")",
"Action", "Actions",
]; ];
const thead = document.createElement("thead"); const thead = document.createElement("thead");
const tr = document.createElement("tr"); const tr = document.createElement("tr");
@@ -47,7 +47,6 @@ export class CostUI {
table.appendChild(tbody); table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table); document.getElementById("cost-items").appendChild(table);
CostUI.addTableStyles(id);
CostUI.createContextMenu(callbacks); CostUI.createContextMenu(callbacks);
table.get_blender_id = function () { table.get_blender_id = function () {
return this.getAttribute("id").split("-")[2]; return this.getAttribute("id").split("-")[2];
@@ -56,20 +55,12 @@ export class CostUI {
return [table, tbody]; return [table, tbody];
} }
static addTableStyles(id) { static deleteCostItem(costItemId, callback) {
const style = document.createElement("style"); const costItemRow = document.getElementById(costItemId);
style.textContent = ` let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {};
#cost-items-${id} th:nth-child(1), expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState);
#cost-items-${id} td:nth-child(1) { localStorage.setItem("expandedState", JSON.stringify(expandedState));
width: auto; callback(costItemId);
}
#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 createContextMenu(callbacks) { static createContextMenu(callbacks) {
@@ -139,21 +130,17 @@ export class CostUI {
addButton.dataset.listenerAdded = "true"; addButton.dataset.listenerAdded = "true";
} }
const deleteCostItem = document.getElementById("delete-cost-item"); const deleteCostItemButton = document.getElementById("delete-cost-item");
if (deleteCostItem && !deleteCostItem.hasListener) { if (deleteCostItemButton && !deleteCostItemButton.hasListener) {
deleteCostItem.addEventListener("click", function () { deleteCostItemButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
let expandedState = CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
JSON.parse(localStorage.getItem("expandedState")) || {};
expandedState = CostUI.deleteCostItemRow(targetRow, expandedState);
localStorage.setItem("expandedState", JSON.stringify(expandedState));
callbacks.deleteCostItem(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
deleteCostItem.hasListener = true; deleteCostItemButton.hasListener = true;
} }
const duplicateButton = document.getElementById("duplicate-button"); const duplicateButton = document.getElementById("duplicate-button");
@@ -175,7 +162,7 @@ export class CostUI {
const targetRow = document.getElementById("context-menu").targetRow; const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) { if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.getSelectedProducts(costItemId); callbacks.enableEditingQuantities(costItemId);
} }
document.getElementById("context-menu").style.display = "none"; document.getElementById("context-menu").style.display = "none";
}); });
@@ -192,7 +179,7 @@ export class CostUI {
document document
.getElementById("cost-items") .getElementById("cost-items")
.addEventListener("dblclick", function (event) { .addEventListener("click", function (event) {
const targetRow = event.target.closest("tr"); const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td"); const targetCell = event.target.closest("td");
if (targetRow && targetCell) { if (targetRow && targetCell) {
@@ -202,11 +189,16 @@ export class CostUI {
const costItemId = parseInt(targetRow.getAttribute("id")); const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex]; const columnName = getColumnNames("cost-items")[columnIndex];
if (columnName === "Cost") { if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId) ? callbacks.enableEditingCostValues(costItemId)
: null; : null;
} }
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
} }
}); });
} }
@@ -363,13 +355,18 @@ export class CostUI {
} }
static createCostSchedule({ static createCostSchedule({
data, costItems,
currency,
costScheduleId, costScheduleId,
blenderID, blenderID,
callbacks = {}, callbacks = {},
}) { }) {
const [table, tbody] = CostUI.createTable(blenderID, callbacks); const [table, tbody] = CostUI.createCostTable(
if (data.length === 0) { blenderID,
currency,
callbacks
);
if (costItems.length === 0) {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
const td = document.createElement("td"); const td = document.createElement("td");
td.colSpan = 6; td.colSpan = 6;
@@ -387,24 +384,29 @@ export class CostUI {
td.appendChild(addSummaryCostItemButton); td.appendChild(addSummaryCostItemButton);
addSummaryCostItemButton.classList.add("action-button"); addSummaryCostItemButton.classList.add("action-button");
} else { } else {
CostUI.createCostItem(data, tbody, 0, null, callbacks); CostUI.createCostTree(costItems, tbody, 0, null, callbacks);
CostUI.applyExpandedState(); CostUI.applyExpandedState();
} }
} }
static createCostItem( static createCostTree(
data, costItems,
container, container,
nestingLevel = 0, nestingLevel = 0,
parentID = null, parentID = null,
callbacks = {} callbacks = {}
) { ) {
data.forEach((costItem) => { costItems.forEach((costItem) => {
const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks); const row = CostUI.addCostItemRow(
costItem,
nestingLevel,
parentID,
callbacks
);
container.appendChild(row); container.appendChild(row);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) { if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
CostUI.createCostItem( CostUI.createCostTree(
costItem.is_nested_by, costItem.is_nested_by,
container, container,
nestingLevel + 1, 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 const totalQuantity = costItem.TotalCostQuantity
? parseFloat(costItem.TotalCostQuantity).toFixed(2) ? parseFloat(costItem.TotalCostQuantity).toFixed(2)
: "-"; : "-";
@@ -431,20 +433,21 @@ export class CostUI {
callbacks callbacks
); );
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity); const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
totalCostQuantityCell.classList.add("clickable-cell");
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol); const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(appliedValue); const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
totalAppliedValueCell.classList.add("clickable-cell");
const totalCostCell = CostUI.createTotalCostCell(costItem); const totalCostCell = CostUI.createTotalCostCell(costItem);
const flexContainerCell = CostUI.createFlexContainerCell( const actionsCell = CostUI.costItemActions(costItem, callbacks);
costItem,
callbacks actionsCell.classList.add("actions-column");
);
row.appendChild(nameCell); row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell); row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell); row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell); row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell); row.appendChild(totalCostCell);
row.appendChild(flexContainerCell); row.appendChild(actionsCell);
row.get_id = function () { row.get_id = function () {
return this.getAttribute("id"); return this.getAttribute("id");
@@ -636,30 +639,92 @@ export class CostUI {
return totalCostCell; return totalCostCell;
} }
static createFlexContainerCell(costItem, callbacks) { static costItemActions(costItem, callbacks) {
const divFlex = document.createElement("div"); const divFlex = document.createElement("div");
divFlex.classList.add("row-container"); divFlex.classList.add("row-container");
const selectButton = CostUI.createSelectButton(costItem, callbacks); const addCostItem = CostUI.addCostItemButton(
divFlex.appendChild(selectButton); 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"); [addCostItem, duplicateButton, selectButton, deleteButton].forEach(
flexContainerCell.appendChild(divFlex); (button) => {
return flexContainerCell; divFlex.appendChild(button);
}
);
const actionsCell = document.createElement("td");
actionsCell.appendChild(divFlex);
return actionsCell;
} }
static createSelectButton(costItem, callbacks) { static duplicateCostItemButton(costItemId, duplicateCostItem) {
const selectButton = document.createElement("button"); const duplicateButton = CostUI.createButton(
selectButton.classList.add("action-button"); "Duplicate",
selectButton.textContent = "Select"; "fa-solid fa-copy"
selectButton.addEventListener("click", function (e) { );
e.stopPropagation(); duplicateButton.addEventListener(
callbacks.selectAssignedElements "click",
? callbacks.selectAssignedElements(costItem.id) duplicateCostItem.bind(null, costItemId)
: null; );
}); 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; 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) { static highlightElement(id) {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
@@ -728,11 +793,15 @@ export class CostUI {
} }
static getRowNameCell(costItemId) { static getRowNameCell(costItemId) {
return document.getElementById(costItemId).querySelector("td input"); return document.getElementById(costItemId)
? document.getElementById(costItemId).querySelector("td input")
: null;
} }
static getCostItemName(costItemId) { static getCostItemName(costItemId) {
return CostUI.getRowNameCell(costItemId).value; return CostUI.getRowNameCell(costItemId)
? CostUI.getRowNameCell(costItemId).value
: "Unnamed";
} }
static createCostValuesForm({ costItemId, costValues, callbacks }) { static createCostValuesForm({ costItemId, costValues, callbacks }) {
@@ -775,7 +844,6 @@ export class CostUI {
static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) { static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) {
const table = CostUI.getCostValuesTable(costItemId); const table = CostUI.getCostValuesTable(costItemId);
if (!table) { if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return; return;
} }
const tr = CostUI.createCostvaluesRow( const tr = CostUI.createCostvaluesRow(
@@ -843,8 +911,8 @@ export class CostUI {
} else if (costValue.applied_value) { } else if (costValue.applied_value) {
costType = "FIXED"; costType = "FIXED";
} }
const options = ["FIXED", "CATEGORY", "SUM"];
const typeCell = CostUI.createTableDropdown("type", costType); const typeCell = CostUI.createTableDropdown("type", options, costType);
const dropdown = typeCell.querySelector("select"); const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function () { dropdown.addEventListener("change", function () {
const selectedType = this.value; 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 cell = document.createElement("td");
const dropdown = document.createElement("select"); const dropdown = document.createElement("select");
dropdown.name = name; dropdown.name = name;
const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach((optionValue) => { options.forEach((optionValue) => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = optionValue; option.value = optionValue;
@@ -1241,7 +1308,7 @@ export class CostUI {
} }
static createProductTable({ static createProductTable({
form, container,
products, products,
quantityNames, quantityNames,
costItemId, costItemId,
@@ -1251,7 +1318,7 @@ export class CostUI {
const noProductsMessage = document.createElement("p"); const noProductsMessage = document.createElement("p");
noProductsMessage.textContent = noProductsMessage.textContent =
"Your Blender Selection is empty! Select objects first."; "Your Blender Selection is empty! Select objects first.";
form.appendChild(noProductsMessage); container.appendChild(noProductsMessage);
return; return;
} }
const { tableContainer, table } = CostUI.addTable({ const { tableContainer, table } = CostUI.addTable({
@@ -1259,7 +1326,7 @@ export class CostUI {
className: "", className: "",
id: "cost-values-table-" + costItemId, id: "cost-values-table-" + costItemId,
}); });
form.appendChild(tableContainer); container.appendChild(tableContainer);
const tbody = document.createElement("tbody"); const tbody = document.createElement("tbody");
table.appendChild(tbody); table.appendChild(tbody);
@@ -1367,7 +1434,7 @@ export class CostUI {
const productIds = products.map((product) => product.info.id); const productIds = products.map((product) => product.info.id);
callbacks.emptyForm = () => { callbacks.emptyForm = () => {
form.innerHTML = ""; container.innerHTML = "";
}; };
const addProductAssignmentsButton = CostUI.addProductAssignmentsButton( const addProductAssignmentsButton = CostUI.addProductAssignmentsButton(
costItemId, costItemId,
@@ -1376,13 +1443,13 @@ export class CostUI {
callbacks callbacks
); );
form.appendChild(quantitySelect); container.appendChild(quantitySelect);
form.appendChild(addProductAssignmentsButton); container.appendChild(addProductAssignmentsButton);
return table; return table;
} }
static createAssignmentsTable({ static createAssignmentsTable({
form, container,
products, products,
costItemId, costItemId,
quantityNames, quantityNames,
@@ -1394,7 +1461,7 @@ export class CostUI {
id: "cost-values-table-" + costItemId, id: "cost-values-table-" + costItemId,
}); });
form.appendChild(tableContainer); container.appendChild(tableContainer);
const tbody = document.createElement("tbody"); const tbody = document.createElement("tbody");
table.appendChild(tbody); table.appendChild(tbody);
@@ -1447,8 +1514,10 @@ export class CostUI {
quantitySelect, quantitySelect,
callbacks callbacks
) { ) {
const addProductAssignmentsButton = document.createElement("button"); const addProductAssignmentsButton = CostUI.createButton(
addProductAssignmentsButton.textContent = "Add Product Assignments"; "Add Product Assignments",
"fa-solid fa-plus"
);
addProductAssignmentsButton.addEventListener("click", (event) => { addProductAssignmentsButton.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
let propName = quantitySelect.value; let propName = quantitySelect.value;
@@ -1463,7 +1532,7 @@ export class CostUI {
}) })
: null; : null;
callbacks.emptyForm ? callbacks.emptyForm() : null; callbacks.emptyForm ? callbacks.emptyForm() : null;
callbacks.getSelectedProducts(costItemId); callbacks.enableEditingQuantities(costItemId);
}); });
addProductAssignmentsButton.classList.add("action-button"); addProductAssignmentsButton.classList.add("action-button");
return addProductAssignmentsButton; return addProductAssignmentsButton;
@@ -1479,6 +1548,7 @@ export class CostUI {
const picker = CostUI.createColorPicker(); const picker = CostUI.createColorPicker();
const tableFontSize = CostUI.createFontSizePicker(); const tableFontSize = CostUI.createFontSizePicker();
const currencyPicker = CostUI.createCurrencyPicker();
settingsMenu.appendChild(picker); settingsMenu.appendChild(picker);
settingsMenu.appendChild(tableFontSize); settingsMenu.appendChild(tableFontSize);
document.getElementById("settings-menu").style.display = "none"; document.getElementById("settings-menu").style.display = "none";
@@ -1521,6 +1591,32 @@ export class CostUI {
return div; 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) { static saveFontSize(fontSize) {
localStorage.setItem("tableFontSize", 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"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): 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) element = tool.Ifc.get_entity(obj)
if not element: if not element:
continue continue
@@ -36,6 +36,7 @@ classes = (
operator.ProfileImportIFC, operator.ProfileImportIFC,
operator.PurgeHdf5Cache, operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass, operator.PurgeUnusedElementsByClass,
operator.PurgeUnusedObjects,
operator.RestartBlender, operator.RestartBlender,
operator.RewindInspector, operator.RewindInspector,
operator.SelectExpressFile, operator.SelectExpressFile,
@@ -32,6 +32,8 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit import ifcopenshell.util.unit
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.debug as core import bonsai.core.debug as core
import bonsai.core.profile
import bonsai.core.type
import bonsai.bim.handler import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc import bonsai.bim.import_ifc as import_ifc
from pathlib import Path from pathlib import Path
@@ -608,6 +610,57 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.get().write(self.filepath) 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): class PipInstall(bpy.types.Operator):
bl_idname = "bim.pip_install" bl_idname = "bim.pip_install"
bl_label = "Pip Install" bl_label = "Pip Install"
@@ -555,6 +555,8 @@ class Scheduler:
if text_color: if text_color:
text_params["fill"] = text_color text_params["fill"] = text_color
text_params["class_"] = "schedule"
if len(text_lines) == 1 and not wrap_text: if len(text_lines) == 1 and not wrap_text:
text_params.update(box_alignment_params) text_params.update(box_alignment_params)
text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params)) text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params))
@@ -36,6 +36,7 @@ classes = (
operator.EnableEditingRepresentationItems, operator.EnableEditingRepresentationItems,
operator.FlipObject, operator.FlipObject,
operator.GetRepresentationIfcParameters, operator.GetRepresentationIfcParameters,
operator.ImportRepresentationItems,
operator.OverrideDelete, operator.OverrideDelete,
operator.OverrideDuplicateMove, operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked, operator.OverrideDuplicateMoveLinked,
@@ -60,6 +61,7 @@ classes = (
operator.UpdateParametricRepresentation, operator.UpdateParametricRepresentation,
operator.UpdateRepresentation, operator.UpdateRepresentation,
prop.RepresentationItem, prop.RepresentationItem,
prop.RepresentationItemObject,
prop.ShapeAspect, prop.ShapeAspect,
prop.BIMObjectGeometryProperties, prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties, prop.BIMGeometryProperties,
@@ -52,7 +52,11 @@ class ViewportData:
("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0), ("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0),
("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 1), ("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 obj_mode
return mesh_modes 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): class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_origin_set" bl_idname = "bim.override_origin_set"
blender_op = bpy.ops.object.origin_set.get_rna_type()
bl_label = "IFC Origin Set" 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"} bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty() 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): def _execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects 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) element = tool.Ifc.get_entity(obj)
if element: if element:
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.")
continue continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") 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: for obj in objects_to_delete:
if element := tool.Ifc.get_entity(obj): if element := tool.Ifc.get_entity(obj):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.")
if collection := obj.BIMObjectProperties.collection: if collection := obj.BIMObjectProperties.collection:
collections_to_delete.discard(collection) collections_to_delete.discard(collection)
continue 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. continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
elif tool.Geometry.is_locked(element): elif tool.Geometry.is_locked(element):
obj.select_set(False) obj.select_set(False)
self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be duplicated.")
continue continue
linked_non_ifc_object = linked and not element linked_non_ifc_object = linked and not element
@@ -1600,6 +1612,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
if not element: if not element:
continue continue
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be edited.")
obj.select_set(False) obj.select_set(False)
continue continue
representation = tool.Geometry.get_active_representation(obj) representation = tool.Geometry.get_active_representation(obj)
@@ -2214,3 +2227,96 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera
if not styled_item.Styles: if not styled_item.Styles:
ifc_file.remove(styled_item) 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 bpy
import bonsai.tool as tool 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 bonsai.bim.module.geometry.data import RepresentationsData, ViewportData
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
@@ -100,6 +100,16 @@ class RepresentationItem(PropertyGroup):
tags: StringProperty(name="Tags") 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): class ShapeAspect(PropertyGroup):
name: StringProperty( name: StringProperty(
name="Name", name="Name",
@@ -137,6 +147,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_triangulation: BoolProperty(name="Force Triangulation", default=False) should_force_triangulation: BoolProperty(name="Force Triangulation", default=False)
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False) is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode) 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: def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
return bool(obj != bpy.context.active_object and obj.data) 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 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_idname = "bim.load_materials"
bl_label = "Load Materials" bl_label = "Load Materials"
bl_description = "Display list of named materials" bl_description = "Display list of named materials"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def execute(self, context):
core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type) 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_idname = "bim.disable_editing_materials"
bl_label = "Disable Editing Materials" bl_label = "Disable Editing Materials"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def execute(self, context):
core.disable_editing_materials(tool.Material) 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_idname = "bim.select_by_material"
bl_label = "Select By Material" bl_label = "Select By Material"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty() 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)) 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_idname = "bim.enable_editing_material"
bl_label = "Enable Editing Material" bl_label = "Enable Editing Material"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty() 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)) core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
return {"FINISHED"}
class EditMaterial(bpy.types.Operator, tool.Ifc.Operator): 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)) 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_idname = "bim.disable_editing_material"
bl_label = "Disable Editing Material" bl_label = "Disable Editing Material"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty() material: bpy.props.IntProperty()
def _execute(self, context): def execute(self, context):
core.disable_editing_material(tool.Material) core.disable_editing_material(tool.Material)
return {"FINISHED"}
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator): class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -776,13 +781,13 @@ class ContractMaterialCategory(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): class EnableEditingMaterialStyle(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_style" bl_idname = "bim.enable_editing_material_style"
bl_label = "Enable Editing Material Style" bl_label = "Enable Editing Material Style"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty() material: bpy.props.IntProperty()
def _execute(self, context): def execute(self, context):
props = bpy.context.scene.BIMMaterialProperties props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = self.material props.active_material_id = self.material
props.editing_material_type = "STYLE" props.editing_material_type = "STYLE"
@@ -800,6 +805,7 @@ class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
style = rep.Items[0].Styles[0] style = rep.Items[0].Styles[0]
if style.Name: # props.styles only has named styles if style.Name: # props.styles only has named styles
props.styles = str(rep.Items[0].Styles[0].id()) props.styles = str(rep.Items[0].Styles[0].id())
return {"FINISHED"}
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
+37 -253
View File
@@ -301,9 +301,10 @@ class ProfileDecorator:
class PolylineDecorator: class PolylineDecorator:
is_installed = False is_installed = False
handlers = [] handlers = []
mouse_pos = None event = None
input_panel = None
input_type = None input_type = None
input_ui = None
angle_snap_mat = None angle_snap_mat = None
angle_snap_loc = None angle_snap_loc = None
use_default_container = False use_default_container = False
@@ -315,11 +316,8 @@ class PolylineDecorator:
if cls.is_installed: if cls.is_installed:
cls.uninstall() cls.uninstall()
handler = cls() 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_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.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True cls.is_installed = True
@@ -333,203 +331,27 @@ class PolylineDecorator:
cls.is_installed = False cls.is_installed = False
@classmethod @classmethod
def set_mouse_position(cls, event): def update(cls, event, tool_state, input_ui, snapping_point):
cls.mouse_pos = event.mouse_region_x, event.mouse_region_y cls.event = event
cls.tool_state = tool_state
cls.input_ui = input_ui
@classmethod @classmethod
def set_input_panel(cls, input_panel, input_type): def set_input_ui(cls, input_ui):
cls.input_panel = input_panel cls.input_ui = input_ui
cls.input_type = input_type
@classmethod @classmethod
def set_angle_axis_line(cls, start, end): def set_angle_axis_line(cls, start, end):
cls.axis_start = start cls.axis_start = start
cls.axis_end = end cls.axis_end = end
@classmethod # @classmethod
def set_axis_rectangle(cls, corners): # def set_axis_rectangle(cls, corners):
cls.axis_rectangle = [*corners] # cls.axis_rectangle = [*corners]
@classmethod @classmethod
def set_use_default_container(cls, value=False): def set_tool_state(cls, tool_state):
cls.use_default_container = value cls.tool_state = tool_state
@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 draw_batch(self, shader_type, content_pos, color, indices=None): def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader shader = self.line_shader if shader_type == "LINES" else self.shader
@@ -537,21 +359,7 @@ class PolylineDecorator:
shader.uniform_float("color", color) shader.uniform_float("color", color)
batch.draw(shader) batch.draw(shader)
@classmethod def draw_input_ui(self, context):
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):
texts = { texts = {
"D": "Distance: ", "D": "Distance: ",
"A": "Angle: ", "A": "Angle: ",
@@ -560,6 +368,7 @@ class PolylineDecorator:
"Z": "Z coord:", "Z": "Z coord:",
"AREA": "Area: ", "AREA": "Area: ",
} }
mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
self.addon_prefs = tool.Blender.get_addon_preferences() self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0 self.font_id = 0
@@ -571,22 +380,21 @@ class PolylineDecorator:
color_highlight = self.addon_prefs.decorator_color_special color_highlight = self.addon_prefs.decorator_color_special
offset = 20 offset = 20
new_line = 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: if key != self.tool_state.input_type:
value = float(value) formatted_value = self.input_ui.get_formatted_value(key)
formatted_value = self.format_input_panel_units(context, value)
else: 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 continue
if key == self.input_type: if key == self.tool_state.input_type:
blf.color(self.font_id, *color_highlight) blf.color(self.font_id, *color_highlight)
else: else:
blf.color(self.font_id, *color) 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.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line * i), 0)
blf.draw(self.font_id, texts[key] + formatted_value) blf.draw(self.font_id, field_name + formatted_value)
def draw_measurements(self, context): def draw_measurements(self, context):
region = context.region region = context.region
@@ -608,9 +416,7 @@ class PolylineDecorator:
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2 pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim) coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
value = measurement_prop[i].dim formatted_value = measurement_prop[i].dim
value = float(value)
formatted_value = self.format_input_panel_units(context, value)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0) blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value) 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.position(self.font_id, coords_angle[0], coords_angle[1], 0)
blf.draw(self.font_id, "a: " + measurement_prop[i].angle) 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): def __call__(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences() 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 default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = [] 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 # When a point is above the plane it projects the point
# to the plane and creates a line # to the plane and creates a line
if snap_prop.snap_type != "Plane" and snap_prop.z != 0: 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.line_shader.uniform_float("lineWidth", 0.75)
self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)]) self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)])
try: # try:
self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)]) # self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
except: # except:
pass # pass
# Area highlight # Area highlight
if "AREA" in list(self.input_panel.keys()): # 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: # if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: # TODO Change to input_ui
edges = [] # edges = []
for i in range(1, len(polyline_points) - 1): # for i in range(1, len(polyline_points) - 1):
edges.append((0, i, i + 1)) # edges.append((0, i, i + 1))
self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) # self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
# Mouse points # Mouse points
if snap_prop.snap_type in ["Face", "Plane"]: 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 mathutils import Vector, Matrix
from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Optional from typing import Optional
from lark import Lark, Transformer 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() 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_idname = "bim.draw_polyline_wall"
bl_label = "Draw Polyline Wall" bl_label = "Draw Polyline Wall"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -294,66 +295,7 @@ class DrawPolylineWall(bpy.types.Operator):
return context.space_data.type == "VIEW_3D" return context.space_data.type == "VIEW_3D"
def __init__(self): def __init__(self):
self.mousemove_count = 0 super().__init__()
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
# TODO This is creating a hack in generate function from DumbWallGenerator # TODO This is creating a hack in generate function from DumbWallGenerator
# Come up with a better solution # Come up with a better solution
@@ -382,187 +324,44 @@ class DrawPolylineWall(bpy.types.Operator):
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"]) DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
def modal(self, context, event): def modal(self, context, event):
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
if not self.is_input_on: self.handle_instructions(context)
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
if self.mousemove_count == 2: self.handle_mouse_move(context, event)
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: self.choose_axis(event)
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"}
if event.value == "RELEASE" and event.type == "BACK_SPACE": self.handle_snap_selection(context, event)
tool.Snap.remove_last_polyline_point()
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type == "LEFTMOUSE": if (
tool.Snap.insert_polyline_point(self.input_panel) not self.tool_state.is_input_on
tool.Blender.update_viewport() and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
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"}:
self.create_walls_from_polyline(context) self.create_walls_from_polyline(context)
context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall() PolylineDecorator.uninstall()
tool.Snap.clear_polyline() tool.Snap.clear_polyline()
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"FINISHED"} return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.handle_keyboard_input(context, event)
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()
if event.value == "PRESS" and event.type == "M": self.handle_inserting_polyline(context, event)
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()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: result = self.handle_cancelation(context, event)
return {"PASS_THROUGH"} if result is not None:
return result
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"}
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def invoke(self, context, event): def invoke(self, context, event):
if context.space_data.type == "VIEW_3D": super().invoke(context, event)
PolylineDecorator.install(context) self.tool_state.use_default_container = True
tool.Snap.set_use_default_container(True) self.tool_state.plane_method = "XY"
PolylineDecorator.set_use_default_container(True) return {"RUNNING_MODAL"}
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"}
class DumbWallAligner: class DumbWallAligner:
@@ -30,7 +30,6 @@ classes = (
operator.EnableEditingArbitraryProfile, operator.EnableEditingArbitraryProfile,
operator.EnableEditingProfile, operator.EnableEditingProfile,
operator.LoadProfiles, operator.LoadProfiles,
operator.PurgeUnusedProfiles,
operator.RemoveProfileDef, operator.RemoveProfileDef,
prop.Profile, prop.Profile,
prop.BIMProfileProperties, prop.BIMProfileProperties,
@@ -29,6 +29,7 @@ def refresh():
class ProfileData: class ProfileData:
data = {} data = {}
failed_previews: set[int] = set()
preview_collection = bpy.utils.previews.new() preview_collection = bpy.utils.previews.new()
is_loaded = False is_loaded = False
@@ -18,6 +18,7 @@
import bpy import bpy
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.element
import bonsai.bim.helper import bonsai.bim.helper
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.bim.module.model.profile as model_profile 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) 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_idname = "bim.enable_editing_profile"
bl_label = "Enable Editing Profile" bl_label = "Enable Editing Profile"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
profile: bpy.props.IntProperty() profile: bpy.props.IntProperty()
def _execute(self, context): def execute(self, context):
props = context.scene.BIMProfileProperties props = context.scene.BIMProfileProperties
props.profile_attributes.clear() props.profile_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes) bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
props.active_profile_id = self.profile props.active_profile_id = self.profile
return {"FINISHED"}
class DisableEditingProfile(bpy.types.Operator): 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) profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points)
else: else:
profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class) 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" profile.ProfileName = "New Profile"
bpy.ops.bim.load_profiles() bpy.ops.bim.load_profiles()
@@ -174,12 +177,12 @@ class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles() 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_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile" bl_label = "Enable Editing Arbitrary Profile"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def execute(self, context):
props = context.scene.BIMProfileProperties props = context.scene.BIMProfileProperties
active_profile = props.profiles[props.active_profile_index] active_profile = props.profiles[props.active_profile_index]
profile_id = active_profile.ifc_definition_id 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") bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context)) ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context))
tool.Blender.set_viewport_tool("bim.cad_tool") tool.Blender.set_viewport_tool("bim.cad_tool")
return {"FINISHED"}
def disable_editing_arbitrary_profile(context): def disable_editing_arbitrary_profile(context):
@@ -210,13 +214,14 @@ def disable_editing_arbitrary_profile(context):
refresh() refresh()
class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): class DisableEditingArbitraryProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_arbitrary_profile" bl_idname = "bim.disable_editing_arbitrary_profile"
bl_label = "Disable Editing Arbitrary Profile" bl_label = "Disable Editing Arbitrary Profile"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def execute(self, context):
return disable_editing_arbitrary_profile(context) disable_editing_arbitrary_profile(context)
return {"FINISHED"}
class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): 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 props.active_arbitrary_profile_id = 0
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile) 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: if not props.profiles:
bpy.ops.bim.load_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) profile = ifc_file.by_id(profile_id)
# generate image # generate image
size = 128 size = 128
img = Image.new("RGBA", (size, size)) img = Image.new("RGBA", (size, size))
draw = ImageDraw.Draw(img) 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] pixels = [item for sublist in img.getdata() for item in sublist]
# save generated image to preview collection # save generated image to preview collection
+12 -9
View File
@@ -43,19 +43,22 @@ class BIM_PT_profiles(Panel):
self.props = context.scene.BIMProfileProperties self.props = context.scene.BIMProfileProperties
active_profile = None 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 preview_collection = ProfileData.preview_collection
box = self.layout.box() box = self.layout.box()
active_profile = self.props.profiles[self.props.active_profile_index]
profile_id = active_profile.ifc_definition_id 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 = self.layout.row(align=True)
row.label(text=f"{ProfileData.data['total_profiles']} Named Profiles", icon="ITALIC") 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.data import LinksData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union from typing import Union
@@ -819,6 +820,7 @@ class LoadProjectElements(bpy.types.Operator):
tool.Project.set_default_context() tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions() tool.Project.set_default_modeling_dimensions()
tool.Root.reload_grid_decorator() tool.Root.reload_grid_decorator()
tool.Root.reload_item_decorator()
return {"FINISHED"} return {"FINISHED"}
def get_decomposition_elements(self): def get_decomposition_elements(self):
@@ -2268,7 +2270,7 @@ if bpy.app.version >= (4, 1, 0):
return True return True
class MeasureTool(bpy.types.Operator): class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.measure_tool" bl_idname = "bim.measure_tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = "Measure Tool" bl_label = "Measure Tool"
@@ -2278,271 +2280,55 @@ class MeasureTool(bpy.types.Operator):
return context.space_data.type == "VIEW_3D" return context.space_data.type == "VIEW_3D"
def __init__(self): def __init__(self):
self.mousemove_count = 0 super().__init__()
self.action_count = 0 self.input_ui = tool.Polyline.create_input_ui(init_z=True)
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", "Z"] 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 self.instructions = """TAB: Cycle Input
M: Modify Snap Point D: Distance Input
C: Close A: Angle Input
Backspace: Remove M: Modify Snap Point
X Y Z: Axis C: Close Polyline
S-(X Y Z): Plane BACKSPACE: Remove Point
Shift: Lock axis X, Y, Z: Choose Axis
""" S-X, S-Y, S-Z: Choose 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
def modal(self, context, event): def modal(self, context, event):
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
if not self.is_input_on: self.handle_instructions(context)
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
if self.mousemove_count == 2: self.handle_mouse_move(context, event)
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: self.choose_axis(event, z=True)
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"}
if event.value == "RELEASE" and event.type == "BACK_SPACE": self.choose_plane(event)
tool.Snap.remove_last_polyline_point()
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type == "LEFTMOUSE": self.handle_snap_selection(context, event)
tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "X": if (
tool.Snap.set_snap_axis_method("X") not self.tool_state.is_input_on
tool.Blender.update_viewport() and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
if event.value == "PRESS" and event.type == "Y": ):
tool.Snap.set_snap_axis_method("Y") context.workspace.status_text_set(text=None)
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"}:
PolylineDecorator.uninstall() PolylineDecorator.uninstall()
tool.Snap.clear_polyline() tool.Snap.clear_polyline()
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"FINISHED"} return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: self.handle_keyboard_input(context, event)
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()
if event.value == "PRESS" and event.type == "M": self.handle_inserting_polyline(context, event)
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()
if event.shift and event.value == "PRESS" and event.type == "X": result = self.handle_cancelation(context, event)
tool.Snap.set_use_default_container(False) if result is not None:
PolylineDecorator.set_use_default_container(False) return result
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"}
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
def invoke(self, context, event): def invoke(self, context, event):
if context.space_data.type == "VIEW_3D": super().invoke(context, event)
PolylineDecorator.install(context) return {"RUNNING_MODAL"}
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"}
+4 -2
View File
@@ -519,5 +519,7 @@ class BIM_PT_purge(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator("bim.purge_unused_profiles") layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
layout.operator("bim.purge_unused_types") 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 @classmethod
def load(cls): def load(cls):
pprops = bpy.context.scene.BIMProfileProperties active_profile = tool.Profile.get_active_profile_ui()
ifc_definition_id = pprops.profiles[pprops.active_profile_index].ifc_definition_id if active_profile:
cls.data = {"psets": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True)} 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 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): class AddProposedProp(bpy.types.Operator):
bl_idname = "bim.add_proposed_prop" bl_idname = "bim.add_proposed_prop"
bl_label = "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"} bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty() obj_type: bpy.props.StringProperty()
@@ -440,5 +448,8 @@ class AddProposedProp(bpy.types.Operator):
prop_value: bpy.props.StringProperty() prop_value: bpy.props.StringProperty()
def execute(self, context): 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"} return {"FINISHED"}
@@ -243,6 +243,7 @@ class PsetProperties(PropertyGroup):
properties: CollectionProperty(name="Properties", type=IfcProperty) properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_pset_name, name="Pset Name") pset_name: EnumProperty(items=get_pset_name, name="Pset Name")
qto_name: EnumProperty(items=get_qto_name, name="Qto Name") qto_name: EnumProperty(items=get_qto_name, name="Qto Name")
# Proposed property.
prop_name: StringProperty(name="Property Name", default="MyProperty") prop_name: StringProperty(name="Property Name", default="MyProperty")
prop_value: StringProperty(name="Property Value", default="Some Value") 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 return False
def draw(self, context): 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() ProfilePsetsData.load()
props = context.scene.ProfilePsetProperties 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"}) is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def _execute(self, context): 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): if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible) obj.hide_set(not self.is_visible)
+36 -2
View File
@@ -31,9 +31,11 @@ from bpy.props import (
CollectionProperty, CollectionProperty,
) )
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.bim.handler
import bonsai.core.geometry import bonsai.core.geometry
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.unit
def get_subelement_class(self, context): def get_subelement_class(self, context):
@@ -124,6 +126,16 @@ def update_spatial_is_locked(self, context):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
else: else:
tool.Geometry.unlock_object(obj) 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): def poll_container_obj(self, obj):
@@ -173,7 +185,18 @@ class Element(PropertyGroup):
class BIMSpatialDecompositionProperties(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"}) container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
containers: CollectionProperty(name="Containers", type=BIMContainer) containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]") contracted_containers: StringProperty(name="Contracted containers", default="[]")
@@ -210,5 +233,16 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
class BIMGridProperties(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) 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() return tool.Ifc.get()
def draw_header(self, context): def draw_header(self, context):
props = context.scene.BIMSpatialDecompositionProperties
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row row.label(text="") # empty text occupies the left of the row
row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_OFF").is_visible = True icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_ON").is_visible = False row.prop(props, "is_visible", text="", icon=icon)
icon = "VIEW_LOCKED" if context.scene.BIMSpatialDecompositionProperties.is_locked else "VIEW_UNLOCKED" icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED"
row.prop(context.scene.BIMSpatialDecompositionProperties, "is_locked", text="", icon=icon) row.prop(props, "is_locked", text="", icon=icon)
def draw(self, context): def draw(self, context):
if not SpatialDecompositionData.is_loaded: 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") self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context): def draw_header(self, context):
props = context.scene.BIMGridProperties
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row row.label(text="") # empty text occupies the left of the row
row.operator("bim.toggle_grids", text="", icon="HIDE_OFF").is_visible = True icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
row.operator("bim.toggle_grids", text="", icon="HIDE_ON").is_visible = False row.prop(props, "is_visible", text="", icon=icon)
icon = "VIEW_LOCKED" if context.scene.BIMGridProperties.is_locked else "VIEW_UNLOCKED" icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED"
row.prop(context.scene.BIMGridProperties, "is_locked", text="", icon=icon) row.prop(props, "is_locked", text="", icon=icon)
class BIM_UL_containers_manager(UIList): class BIM_UL_containers_manager(UIList):
+17 -11
View File
@@ -356,14 +356,14 @@ class BrowseExternalStyle(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator): class ActivateExternalStyle(bpy.types.Operator):
bl_idname = "bim.activate_external_style" bl_idname = "bim.activate_external_style"
bl_label = "Activate External Style" bl_label = "Activate External Style"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
material_name: bpy.props.StringProperty(name="Material Name", default="") material_name: bpy.props.StringProperty(name="Material Name", default="")
def _execute(self, context): def execute(self, context):
if not self.material_name: if not self.material_name:
material = context.active_object.active_material material = context.active_object.active_material
else: else:
@@ -403,6 +403,7 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
if material.use_nodes: if material.use_nodes:
tool.Blender.copy_node_graph(material, db["data_block"]) tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"]) bpy.data.materials.remove(db["data_block"])
return {"FINISHED"}
def copy_material_attributes(self, source, target): def copy_material_attributes(self, source, target):
ID_properties = bpy.types.ID.bl_rna.properties ID_properties = bpy.types.ID.bl_rna.properties
@@ -444,33 +445,37 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
set_prop(prop_name) set_prop(prop_name)
class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator): class DisableEditingStyles(bpy.types.Operator):
bl_idname = "bim.disable_editing_styles" bl_idname = "bim.disable_editing_styles"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_label = "Disable Editing Styles" bl_label = "Disable Editing Styles"
def _execute(self, context): def execute(self, context):
core.disable_editing_styles(tool.Style) 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_idname = "bim.load_styles"
bl_label = "Load Styles" bl_label = "Load Styles"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
style_type: bpy.props.StringProperty() style_type: bpy.props.StringProperty()
def _execute(self, context): def execute(self, context):
core.load_styles(tool.Style, style_type=self.style_type) 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_idname = "bim.select_by_style"
bl_label = "Select By Style" bl_label = "Select By Style"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty() 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)) core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style))
return {"FINISHED"}
class ChooseTextureMapPath(bpy.types.Operator): 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) 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_idname = "bim.enable_editing_surface_style"
bl_label = "Enable Editing Surface Style" bl_label = "Enable Editing Surface Style"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(default=0) style: bpy.props.IntProperty(default=0)
ifc_class: bpy.props.StringProperty(default="") ifc_class: bpy.props.StringProperty(default="")
def _execute(self, context): def execute(self, context):
props = bpy.context.scene.BIMStylesProperties props = bpy.context.scene.BIMStylesProperties
style = tool.Ifc.get().by_id(self.style) style = tool.Ifc.get().by_id(self.style)
props.is_editing_style = 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" and active_style_type != "Shading"
): ):
tool.Style.switch_shading(material, "Shading") tool.Style.switch_shading(material, "Shading")
return {"FINISHED"}
class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
@@ -26,7 +26,6 @@ classes = (
operator.DisableEditingType, operator.DisableEditingType,
operator.DuplicateType, operator.DuplicateType,
operator.EnableEditingType, operator.EnableEditingType,
operator.PurgeUnusedTypes,
operator.RemoveType, operator.RemoveType,
operator.RenameType, operator.RenameType,
operator.SelectSimilarType, operator.SelectSimilarType,
+3 -12
View File
@@ -515,12 +515,12 @@ class RenameType(bpy.types.Operator, tool.Ifc.Operator):
self.layout.prop(self, "name") 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_idname = "bim.auto_rename_occurrences"
bl_label = "Auto Rename Occurrences" bl_label = "Auto Rename Occurrences"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): def execute(self, context):
obj = context.active_object obj = context.active_object
element_type = tool.Ifc.get_entity(obj) element_type = tool.Ifc.get_entity(obj)
if element_type and element_type.is_a("IfcTypeObject"): 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()) occurrence.Name = tool.Model.generate_occurrence_name(element_type, occurrence.is_a())
if obj: if obj:
tool.Root.set_object_name(obj, occurrence) tool.Root.set_object_name(obj, occurrence)
return {"FINISHED"}
class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): 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.ifc_class = new.is_a()
context.scene.BIMModelProperties.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id) context.scene.BIMModelProperties.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"} 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: 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. :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) 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) 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( def unshare_pset(
+14
View File
@@ -581,6 +581,16 @@ class Patch:
def run_migrate_patch(cls, infile, outfile, schema): pass 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 @interface
class Owner: class Owner:
def add_address_attribute(cls, name): pass def add_address_attribute(cls, name): pass
@@ -668,10 +678,12 @@ class Qto:
def get_rounded_value(cls, new_quantity): pass def get_rounded_value(cls, new_quantity): pass
def set_qto_result(cls, result): pass def set_qto_result(cls, result): pass
@interface @interface
class Raycast: class Raycast:
pass pass
@interface @interface
class Resource: class Resource:
def clear_productivity_data(cls, props): pass def clear_productivity_data(cls, props): pass
@@ -945,10 +957,12 @@ class Spatial:
class Covering: class Covering:
def get_z_from_ceiling_height(cls): pass def get_z_from_ceiling_height(cls): pass
@interface @interface
class Snap: class Snap:
pass pass
@interface @interface
class Structural: class Structural:
def disable_editing_structural_analysis_model(cls): pass 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.nest import Nest
from bonsai.tool.owner import Owner from bonsai.tool.owner import Owner
from bonsai.tool.patch import Patch from bonsai.tool.patch import Patch
from bonsai.tool.polyline import Polyline
from bonsai.tool.project import Project from bonsai.tool.project import Project
from bonsai.tool.profile import Profile from bonsai.tool.profile import Profile
from bonsai.tool.pset import Pset from bonsai.tool.pset import Pset
+1 -1
View File
@@ -803,7 +803,7 @@ class Blender(bonsai.core.tool.Blender):
return collections_mapping return collections_mapping
@classmethod @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: if obj.type not in cls.OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE:
return False return False
if not (element := tool.Ifc.get_entity(obj)): 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) unit = cls.get_quantity_unit_symbol(quantity)
return selected_quantitites, unit 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 @classmethod
def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]: def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]:
if related_object_type == "PRODUCT": if related_object_type == "PRODUCT":
@@ -859,7 +867,32 @@ class Cost(bonsai.core.tool.Cost):
bpy.ops.bim.connect_websocket_server(page="costing") bpy.ops.bim.connect_websocket_server(page="costing")
cost_schedule_data = cls.create_cost_schedule_json(cost_chedule) cost_schedule_data = cls.create_cost_schedule_json(cost_chedule)
tool.Web.send_webui_data( 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", data_key="cost_items",
event="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}") print(f"{class_string: <50} {unused[ifc_class]: >5}")
return sum(unused.values()) 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() element_obj_names = set()
for element in filtered_elements: for element in filtered_elements:
obj = tool.Ifc.get_object(element) obj = tool.Ifc.get_object(element)
if not obj:
continue
current_representation = tool.Geometry.get_active_representation(obj) current_representation = tool.Geometry.get_active_representation(obj)
if current_representation: if current_representation:
subcontext = current_representation.ContextOfItems 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 # You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.api.profile import ifcopenshell.api.profile
import ifcopenshell.geom import ifcopenshell.geom
@@ -39,7 +40,11 @@ class Profile(bonsai.core.tool.Profile):
settings = ifcopenshell.geom.settings() settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile) shape = ifcopenshell.geom.create_shape(settings, profile)
verts = shape.verts verts = shape.verts
if not verts:
raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
edges = shape.edges edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)] 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. # In UI unnamed profiles are not available, so we don't handle them.
new_profile.ProfileName = profile.ProfileName + "_copy" new_profile.ProfileName = profile.ProfileName + "_copy"
return new_profile 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) return bonsai.bim.schema.ifc.psetqto.get_by_name(name)
@classmethod @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): if props.properties.get(name):
return return f"Property '{name}' already exists."
prop = props.properties.add() prop = props.properties.add()
prop.name = name prop.name = name
metadata = prop.metadata metadata = prop.metadata
@@ -327,4 +327,3 @@ class Pset(bonsai.core.tool.Pset):
return value return value
except: except:
return value return value
return value
+7
View File
@@ -28,6 +28,7 @@ import bonsai.core.geometry
import bonsai.tool as tool import bonsai.tool as tool
from typing import Union, Optional, Any from typing import Union, Optional, Any
from bonsai.bim.module.spatial.decorator import GridDecorator from bonsai.bim.module.spatial.decorator import GridDecorator
from bonsai.bim.module.geometry.decorator import ItemDecorator
class Root(bonsai.core.tool.Root): class Root(bonsai.core.tool.Root):
@@ -214,6 +215,12 @@ class Root(bonsai.core.tool.Root):
new.obj = obj new.obj = obj
GridDecorator.install(bpy.context) 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 @classmethod
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None: def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
destination_obj.data = source_obj.data 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): class Snap(bonsai.core.tool.Snap):
mouse_pos = None tool_state = None
snap_angle = None
use_default_container = False
snap_plane_method = None snap_plane_method = None
snap_axis_method = None
@classmethod @classmethod
def set_use_default_container(cls, value=True): def set_tool_state(cls, tool_state):
cls.use_default_container = value cls.tool_state = tool_state
@classmethod @classmethod
def set_snap_plane_method(cls, value=True): def set_snap_plane_method(cls, value=True):
@@ -48,13 +45,6 @@ class Snap(bonsai.core.tool.Snap):
return return
cls.snap_plane_method = value 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 @classmethod
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
matrix = obj.matrix_world.copy() matrix = obj.matrix_world.copy()
@@ -70,6 +60,7 @@ class Snap(bonsai.core.tool.Snap):
return snap_point return snap_point
# TODO Remove this function
@classmethod @classmethod
def select_snap_point(cls, snap_points, hit, threshold): def select_snap_point(cls, snap_points, hit, threshold):
shortest_distance = None shortest_distance = None
@@ -98,16 +89,15 @@ class Snap(bonsai.core.tool.Snap):
except: except:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point.add() 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.x = snap_point[0]
snap_vertex.y = snap_point[1] snap_vertex.y = snap_point[1]
snap_vertex.z = snap_point[2] snap_vertex.z = snap_point[2]
snap_vertex.snap_type = snap_type snap_vertex.snap_type = snap_type
@classmethod
def clear_snapping_point(cls):
bpy.context.scene.BIMModelProperties.snap_mouse_point.clear()
@classmethod @classmethod
def update_snapping_ref(cls, snap_point, snap_type): def update_snapping_ref(cls, snap_point, snap_type):
try: try:
@@ -125,18 +115,18 @@ class Snap(bonsai.core.tool.Snap):
bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear() bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear()
@classmethod @classmethod
def insert_polyline_point(cls, input_panel): def insert_polyline_point(cls, input_ui):
x = float(input_panel["X"]) x = input_ui.get_number_value("X")
y = float(input_panel["Y"]) y = input_ui.get_number_value("Y")
try: if input_ui.get_number_value("Z") is not None:
z = float(input_panel["Z"]) z = input_ui.get_number_value("Z")
except: else:
z = Vector((0, 0, 0)) z = 0
d = input_panel["D"] d = input_ui.get_formatted_value("D")
a = input_panel["A"] a = input_ui.get_formatted_value("A")
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] 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 z = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
if x is None and y is None: 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) polyline_measurement.remove(len(polyline_measurement) - 1)
@classmethod @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): def create_axis_line_data(rot_mat, origin):
length = 1000 length = 1000
direction = Vector((1, 0, 0)) 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)) direction = Vector((0, 0, 1))
rot_dir = rot_mat.inverted() @ direction rot_dir = rot_mat.inverted() @ direction
start = origin + rot_dir * length start = origin + rot_dir * length
@@ -202,13 +192,13 @@ class Snap(bonsai.core.tool.Snap):
def create_axis_rectangle_data(origin): def create_axis_rectangle_data(origin):
size = 0.5 size = 0.5
direction = Vector((1, 0, 0)) direction = Vector((1, 0, 0))
if cls.snap_plane_method == "YZ": if tool_state.plane_method == "YZ":
direction = Vector((0, 0, 1)) direction = Vector((0, 0, 1))
rot_mat = Matrix.Rotation(math.radians(360), 3, pivot_axis) rot_mat = Matrix.Rotation(math.radians(360), 3, pivot_axis)
rot_dir = rot_mat.inverted() @ direction rot_dir = rot_mat.inverted() @ direction
v1 = origin + rot_dir * 0 v1 = origin + rot_dir * 0
v2 = origin + rot_dir * size v2 = origin + rot_dir * size
if cls.snap_plane_method == "XY": if tool_state.plane_method == "XY":
angle = 270 angle = 270
else: else:
angle = 90 angle = 90
@@ -230,17 +220,17 @@ class Snap(bonsai.core.tool.Snap):
# Translates intersection point based on last_point # Translates intersection point based on last_point
translated_intersection = intersection - last_point translated_intersection = intersection - last_point
snap_axis = [] snap_axis = []
if not lock_axis: if not tool_state.snap_angle:
for i in range(1, 13): for i in range(1, 25):
angle = 30 * i angle = 15 * i
snap_axis.append(angle) snap_axis.append(angle)
else: else:
snap_axis = [lock_axis] snap_axis = [tool_state.snap_angle]
pivot_axis = "Z" pivot_axis = "Z"
if cls.snap_plane_method == "XZ": if tool_state.plane_method == "XZ":
pivot_axis = "Y" pivot_axis = "Y"
if cls.snap_plane_method == "YZ": if tool_state.plane_method == "YZ":
pivot_axis = "X" pivot_axis = "X"
for axis in snap_axis: 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) start, end = create_axis_line_data(rot_mat, last_point)
rot_intersection = rot_mat @ translated_intersection rot_intersection = rot_mat @ translated_intersection
proximity = rot_intersection.y proximity = rot_intersection.y
if cls.snap_plane_method == "XZ": if tool_state.plane_method == "XZ":
proximity = rot_intersection.z proximity = rot_intersection.z
PolylineDecorator.set_angle_axis_line(start, end) PolylineDecorator.set_angle_axis_line(start, end)
if lock_axis: if lock_angle:
is_on_rot_axis = True is_on_rot_axis = True
else: else:
is_on_rot_axis = abs(proximity) <= 0.15 is_on_rot_axis = abs(proximity) <= 0.15
@@ -259,7 +249,7 @@ class Snap(bonsai.core.tool.Snap):
if is_on_rot_axis: if is_on_rot_axis:
# Snap to axis # Snap to axis
rot_intersection = Vector((rot_intersection.x, 0, rot_intersection.z)) 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)) rot_intersection = Vector((rot_intersection.x, rot_intersection.y, 0))
# Convert it back # Convert it back
snap_intersection = rot_mat.inverted() @ rot_intersection + last_point snap_intersection = rot_mat.inverted() @ rot_intersection + last_point
@@ -280,11 +270,10 @@ class Snap(bonsai.core.tool.Snap):
return sorted_intersections[0], "Mix" return sorted_intersections[0], "Mix"
@classmethod @classmethod
def detect_snapping_points(cls, context, event, objs_2d_bbox): def detect_snapping_points(cls, context, event, objs_2d_bbox, tool_state):
region = context.region
rv3d = context.region_data rv3d = context.region_data
space = context.space_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 = [] detected_snaps = []
snap_threshold = 0.3 snap_threshold = 0.3
@@ -306,14 +295,16 @@ class Snap(bonsai.core.tool.Snap):
plane_origin = Vector((0, 0, 0)) plane_origin = Vector((0, 0, 0))
plane_normal = Vector((0, 0, 1)) plane_normal = Vector((0, 0, 1))
if not cls.snap_plane_method: if not tool_state.plane_method:
camera_rotation = rv3d.view_rotation camera_rotation = rv3d.view_rotation
plane_origin = Vector((0, 0, 0)) plane_origin = Vector((0, 0, 0))
view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed() view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed()
plane_normal = view_direction.normalized() 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 tool_state.plane_method == "XY" or (
if cls.use_default_container: 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)) plane_origin = Vector((0, 0, elevation))
elif not last_polyline_point: elif not last_polyline_point:
plane_origin = Vector((0, 0, 0)) 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_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 0, 1)) 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: if last_polyline_point:
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 1, 0)) plane_normal = Vector((0, 1, 0))
elif cls.snap_plane_method == "YZ": elif tool_state.plane_method == "YZ":
if last_polyline_point: if last_polyline_point:
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((1, 0, 0)) plane_normal = Vector((1, 0, 0))
return plane_origin, plane_normal 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_length_squared = 1.0
best_obj = None best_obj = None
best_hit = 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) hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
if hit is None: if hit is None:
# Tried original mouse position. Now it will try the offsets. # 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: for value in mouse_offset:
cls.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) 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) hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos)
if hit: if hit:
break break
cls.mouse_pos = original_mouse_pos mouse_pos = original_mouse_pos
if hit is not None: if hit is not None:
hit_world = obj.original.matrix_world @ hit hit_world = obj.original.matrix_world @ hit
@@ -371,14 +362,14 @@ class Snap(bonsai.core.tool.Snap):
objs_to_raycast = [] objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox: for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d: 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 space.local_view:
if obj.local_view_get(context.space_data): if obj.local_view_get(context.space_data):
objs_to_raycast.append(obj) objs_to_raycast.append(obj)
else: else:
objs_to_raycast.append(obj) objs_to_raycast.append(obj)
# 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: if hit is not None:
detected_snaps.append({"Object": (snap_obj, hit, face_index)}) 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 elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin, plane_normal = select_plane_method() 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) intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal)
axis_start = None 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 # TODO It only work for XY plane. Make it work also for None plane_method
rot_intersection = None rot_intersection = None
if not cls.snap_plane_method: if not tool_state.plane_method:
if cls.snap_axis_method == "X": if tool_state.axis_method == "X":
cls.snap_angle = 180 tool_state.snap_angle = 180
if cls.snap_axis_method == "Y": if tool_state.axis_method == "Y":
cls.snap_angle = 90 tool_state.snap_angle = 90
if cls.snap_axis_method == "Z": if tool_state.axis_method == "Z":
cls.snap_angle = 90 tool_state.snap_angle = 90
if cls.snap_axis_method: if tool_state.axis_method:
rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
if cls.snap_plane_method:
if cls.snap_plane_method in {"XY", "XZ"} and cls.snap_axis_method == "X":
cls.snap_angle = 180
if cls.snap_plane_method in {"XY", "YZ"} and cls.snap_axis_method == "Y":
cls.snap_angle = 90
if cls.snap_plane_method in {"YZ"} and cls.snap_axis_method == "Z":
cls.snap_angle = 180
if cls.snap_plane_method in {"XZ"} and cls.snap_axis_method == "Z":
cls.snap_angle = 90
if event.shift or cls.snap_axis_method:
# Doesn't update snap_angle so that it keeps in the same axis # 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: 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: if rot_intersection:
detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)}) detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)})
@@ -445,7 +438,7 @@ class Snap(bonsai.core.tool.Snap):
return detected_snaps return detected_snaps
@classmethod @classmethod
def select_snapping_points(cls, context, event, detected_snaps): def select_snapping_points(cls, context, event, tool_state, detected_snaps):
snapping_points = [] snapping_points = []
for origin in detected_snaps: for origin in detected_snaps:
if "Object" in list(origin.keys()): if "Object" in list(origin.keys()):
@@ -477,10 +470,6 @@ class Snap(bonsai.core.tool.Snap):
snapping_points.append(op) snapping_points.append(op)
break break
if "Plane" in list(origin.keys()):
intersection = origin["Plane"]
snapping_points.append((intersection, "Plane"))
for origin in detected_snaps: for origin in detected_snaps:
if "Axis" in list(origin.keys()): if "Axis" in list(origin.keys()):
intersection = origin["Axis"] intersection = origin["Axis"]
@@ -488,8 +477,12 @@ class Snap(bonsai.core.tool.Snap):
axis_end = intersection[2] axis_end = intersection[2]
snapping_points.append((intersection[0], "Axis")) snapping_points.append((intersection[0], "Axis"))
if "Plane" in list(origin.keys()):
intersection = origin["Plane"]
snapping_points.append((intersection, "Plane"))
# Make Axis first priority # 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]) cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1])
for point in snapping_points: for point in snapping_points:
if point[1] == "Axis": if point[1] == "Axis":
@@ -508,131 +501,3 @@ class Snap(bonsai.core.tool.Snap):
shifted_list = snapping_points[1:] + snapping_points[:1] shifted_list = snapping_points[1:] + snapping_points[:1]
cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1]) cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1])
return shifted_list 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): class Spatial(bonsai.core.tool.Spatial):
@classmethod @classmethod
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: 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 := tool.Ifc.get_entity(element_obj)):
if not element:
return False return False
if tool.Ifc.get_schema() == "IFC2X3": if tool.Ifc.get_schema() == "IFC2X3":
if not container.is_a("IfcSpatialStructureElement"): 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. 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() ifc_file = tool.Ifc.get()
if operator_data["type"] == "getPredefinedTypes": if operator_data["type"] == "getPredefinedTypes":
print("getting predefined types") print("getting predefined types")
@@ -425,31 +379,6 @@ class Web(bonsai.core.tool.Web):
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"]) cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True) products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=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": if operator_data["type"] == "addSummaryCostItem":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"]) cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule) 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"] prop_name = operator_data["propName"]
if prop_name == "count": if prop_name == "count":
prop_name = "" prop_name = ""
print(prop_name)
bpy.ops.bim.assign_cost_item_quantity( bpy.ops.bim.assign_cost_item_quantity(
cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name 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"])) 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) 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 @classmethod
def load_cost_schedule_web_ui(cls, cost_schedule): def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule) json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data( 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", data_key="cost_items",
event="cost_items", event="cost_items",
) )
+15 -3
View File
@@ -24,22 +24,29 @@ import ifcopenshell.api.pset
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcopenshell.util.shape
import ifcopenshell.util.representation import ifcopenshell.util.representation
import multiprocessing import multiprocessing
from collections import namedtuple from collections import namedtuple
from typing import Any from typing import Any, Literal, get_args
Function = namedtuple("Function", ["measure", "name", "description"]) 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__)) 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: with open(os.path.join(cwd, name + ".json"), "r") as f:
rules[name] = json.load(f) rules[name] = json.load(f)
def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict: def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict:
"""
:param rules: Set of rules from `ifc5d.qto.rules`.
"""
results = {} results = {}
for calculator, queries in rules["calculators"].items(): for calculator, queries in rules["calculators"].items():
calculator = calculators[calculator] 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: 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 element, qtos in results.items():
for name, quantities in qtos.items(): for name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False) 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/IfcGeomElement.h"
#include "../ifcgeom/ConversionSettings.h" #include "../ifcgeom/ConversionSettings.h"
#include "../ifcgeom/abstract_mapping.h" #include "../ifcgeom/abstract_mapping.h"
#include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE #ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/kernels/opencascade/OpenCascadeKernel.h" #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) { 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; expl->instance = item->instance;
return convert(expl, cs); return convert(expl, cs);
} }
+4
View File
@@ -701,6 +701,10 @@ namespace IfcGeom {
/// Gets the representation of the current geometrical entity. /// Gets the representation of the current geometrical entity.
Element* get() Element* get()
{ {
if (!initialization_outcome_) {
throw std::runtime_error("Iterator not initialized");
}
auto ret = *task_result_iterator_; auto ret = *task_result_iterator_;
// If we want to organize the element considering their hierarchy // 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) { for (auto it = loft->children.begin(); it < loft->children.end() - 1; ++it) {
auto jt = it + 1; 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_Shape, 2> shps;
std::array<TopoDS_Wire, 2> ws; std::array<TopoDS_Wire, 2> ws;
for (int i = 0; i < 2; ++i) { for (int i = 0; i < 2; ++i) {
if (!convert(fa[i], shps[i])) { if (fa[i]->kind() == taxonomy::FACE) {
return false; 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; return false;
} }
// @todo this is only outer wire // @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()) { if (shps[0].ShapeType() == TopAbs_FACE) {
// faces.Append(shps[0]); // When processing a sectioned *surface* there are no
BB.Add(comp, shps[0]); // begin and end caps that need to be added.
} if (it == loft->children.begin()) {
if (jt == loft->children.end() - 1) { // faces.Append(shps[0]);
// faces.Append(shps[1]); BB.Add(comp, shps[0]);
BB.Add(comp, shps[1]); }
if (jt == loft->children.end() - 1) {
// faces.Append(shps[1]);
BB.Add(comp, shps[1]);
}
} }
BRepTools_WireExplorer a(ws[0]); BRepTools_WireExplorer a(ws[0]);
BRepTools_WireExplorer b(ws[1]); BRepTools_WireExplorer b(ws[1]);
+1 -1
View File
@@ -92,7 +92,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve* inst) {
return loop; return loop;
} }
else { 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; 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; taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(fabs(length), fn); 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; return pwf;
} }
+4 -1
View File
@@ -39,6 +39,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
#endif #endif
if (has_position) { if (has_position) {
m4 = taxonomy::cast<taxonomy::matrix4>(map(inst->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) { if (ry > rx) {
@@ -58,9 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
auto el = taxonomy::make<taxonomy::ellipse>(); auto el = taxonomy::make<taxonomy::ellipse>();
el->radius = rx; el->radius = rx;
el->radius2 = ry; el->radius2 = ry;
el->matrix = m4;
ed->basis = el; ed->basis = el;
lp->children.push_back(ed); lp->children.push_back(ed);
fc->children.push_back(lp); fc->children.push_back(lp);
fc->matrix = m4;
return fc; return fc;
} }
@@ -18,6 +18,7 @@
********************************************************************************/ ********************************************************************************/
#include "mapping.h" #include "mapping.h"
#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping) #define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry; 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 // @todo currently only the case is handled where directrix returns a piecewise_function
if (auto pwf = taxonomy::dcast<taxonomy::piecewise_function>(dir)) { if (auto pwf = taxonomy::dcast<taxonomy::piecewise_function>(dir)) {
piecewise_function_evaluator evaluator(pwf,&settings_);
double start = 0; double start = 0;
double end = pwf->length(); double end = pwf->length();
#ifdef SCHEMA_HAS_IfcDirectrixCurveSweptAreaSolid #ifdef SCHEMA_HAS_IfcDirectrixCurveSweptAreaSolid
@@ -53,20 +55,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid
} }
} }
#endif #endif
auto curve_length = end - start; auto evaluation_points = evaluator.evaluation_points();
auto param_type = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepType>().get(); for (const auto& dist_along : evaluation_points) {
auto param = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepParam>().get(); auto m4 = evaluator.evaluate(dist_along);
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);
/* /*
std::stringstream ss; std::stringstream ss;
+7 -5
View File
@@ -18,6 +18,7 @@
********************************************************************************/ ********************************************************************************/
#include "mapping.h" #include "mapping.h"
#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping) #define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry; 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 double gradient_start = m(0, 3); // start of vertical (row 0, col 3) - "Distance Along" horizontal curve
// create the vertical pwf // 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 // Determine the valid domain of the PWF... the valid domain is where both
// the base curve and gradient curves are defined // 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 // 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) // u is distance from start of gradient curve (vertical)
// add vertical->start() to u to get distance from start of horizontal // add vertical->start() to u to get distance from start of horizontal
auto xy = horizontal->evaluate(u + vertical->start()); auto xy = horizontal_evaluator.evaluate(u + start);
auto uz = vertical->evaluate(u); 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(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 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; taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(length, composition); 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; return pwf;
} }
@@ -19,6 +19,7 @@
#include "mapping.h" #include "mapping.h"
#include "../profile_helper.h" #include "../profile_helper.h"
#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping) #define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry; using namespace ifcopenshell::geometry;
@@ -144,11 +145,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst
offset_spans.emplace_back(l, fn); 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 { piecewise_function_evaluator pw_evaluator(pw_curve, &settings_), offsets_evaluator(offsets, &settings_);
auto p = pw_curve->evaluate(u); auto composition = [pw_evaluator, offsets_evaluator](double u) -> Eigen::Matrix4d {
auto offset = offsets->evaluate(u); auto p = pw_evaluator.evaluate(u);
auto offset = offsets_evaluator.evaluate(u);
Eigen::Matrix4d m = p * offset; Eigen::Matrix4d m = p * offset;
return m; 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 // this may change depending on decisions in the bSI-IF
taxonomy::piecewise_function::spans_t spans; taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(basis_curve_length, composition); 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; return pwf;
} }
@@ -19,6 +19,7 @@
#include "mapping.h" #include "mapping.h"
#include "../profile_helper.h" #include "../profile_helper.h"
#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping) #define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry; using namespace ifcopenshell::geometry;
@@ -31,7 +32,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPointByDistanceExpression* i
//auto item = map(basis_curve); //auto item = map(basis_curve);
//auto pw_curve = ifcopenshell::geometry::piecewise_from_item(item); //auto pw_curve = ifcopenshell::geometry::piecewise_from_item(item);
auto pw_curve = taxonomy::dcast<taxonomy::piecewise_function>(map(inst->BasisCurve())); 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 o = m.col(3).head<3>();
auto z = m.col(2).head<3>(); auto z = m.col(2).head<3>();
@@ -22,28 +22,10 @@
using namespace ifcopenshell::geometry; using namespace ifcopenshell::geometry;
#include "../../ifcgeom/profile_helper.h" #include "../../ifcgeom/profile_helper.h"
#include "../../ifcgeom/infra_sweep_helper.h"
#include <boost/range/combine.hpp>
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal #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) { taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* inst) {
std::vector<cross_section> cross_sections; 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()); return make_loft(settings_, inst, pwf, cross_sections);
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;
} }
#endif #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) #define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry; using namespace ifcopenshell::geometry;
#include "../piecewise_function_evaluator.h"
#ifdef SCHEMA_HAS_IfcSegmentedReferenceCurve #ifdef SCHEMA_HAS_IfcSegmentedReferenceCurve
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* inst) { 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(); const Eigen::Matrix4d& m = p->ccomponents();
double cant_start = m(0, 3); // start of cant curve 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 // Determine the valid domain of the PWF... the valid domain is where
// horizontal, gradient and cant curves are defined // 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 // 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 // u is distance from start of cant curve
// add cant->start() to u to get the distance from start of gradient curve // add cant->start() to u to get the distance from start of gradient curve
auto g = gradient->evaluate(u+cant->start()); auto g = gradient_evaluator.evaluate(u + start);
auto c = cant->evaluate(u); auto c = cant_evaluator.evaluate(u);
// Need to multiply g and c so the axis vectors // Need to multiply g and c so the axis vectors
// from cant have the correct rotation applied so // 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; taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(length, composition); 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; return pwf;
} }
+3
View File
@@ -135,6 +135,9 @@ BIND(IfcFixedReferenceSweptAreaSolid)
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal #ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal
BIND(IfcSectionedSolidHorizontal) BIND(IfcSectionedSolidHorizontal)
#endif #endif
#ifdef SCHEMA_HAS_IfcSectionedSurface
BIND(IfcSectionedSurface)
#endif
BIND(IfcCircle); BIND(IfcCircle);
BIND(IfcEllipse); 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 { namespace taxonomy {
std::vector<double> ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points() const { piecewise_function_impl::piecewise_function_impl(double start, const spans_t& s) : start_(start), spans_(s) {
if (!eval_points_.has_value()) { }
double curve_length = length();
auto param_type = settings_ ? settings_->get<ifcopenshell::geometry::settings::PiecewiseStepType>().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE; piecewise_function_impl::piecewise_function_impl(double start, const std::vector<piecewise_function::ptr>& pwfs) : start_(start) {
auto param = settings_ ? settings_->get<ifcopenshell::geometry::settings::PiecewiseStepParam>().get() : 0.5; for (auto& pwf : pwfs) {
unsigned num_steps = 0; spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end());
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);
} }
return *eval_points_;
} }
std::vector<double> ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points(double ustart, double uend, unsigned nsteps) const { const piecewise_function_impl::spans_t& piecewise_function_impl::spans() const { return spans_; }
double curve_length = length();
ustart = std::max(start_, ustart);
uend = std::min(uend, start_ + curve_length);
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; double piecewise_function_impl::start() const {
return start_;
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;
} }
ifcopenshell::geometry::taxonomy::item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate() const { double piecewise_function_impl::end() const {
return evaluate(evaluation_points()); return start_ + length();
} }
item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double ustart, double uend, unsigned nsteps) const { double piecewise_function_impl::length() const {
return evaluate(evaluation_points(ustart, uend, nsteps)); 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 { piecewise_function_impl* piecewise_function_impl::clone_() const { return new piecewise_function_impl(*this); }
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};
}
} // namespace taxonomy } // namespace taxonomy
+9 -65
View File
@@ -12,79 +12,23 @@ namespace taxonomy {
struct piecewise_function_impl { struct piecewise_function_impl {
using spans_t = std::vector<std::pair<double, std::function<Eigen::Matrix4d(double u)>>>; 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), piecewise_function_impl(double start, const spans_t& s);
settings_(settings), piecewise_function_impl(double start, const std::vector<piecewise_function::ptr>& pwfs);
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(piecewise_function_impl&&) = default; piecewise_function_impl(piecewise_function_impl&&) = default;
piecewise_function_impl(const piecewise_function_impl&) = default; piecewise_function_impl(const piecewise_function_impl&) = default;
const ifcopenshell::geometry::Settings* settings_ = nullptr; const spans_t& spans() const;
bool is_empty() const;
const spans_t& spans() const { return spans_; } double start() const;
double end() const;
bool is_empty() const { return spans_.empty(); } double length() const;
piecewise_function_impl* clone_() const;
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;
private: 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 double start_ = 0.0; // starting value of the pwf
spans_t spans_; spans_t spans_;
mutable double current_span_start_ = 0; //mutable boost::optional<double> length_; // used for length() method
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_;
}; };
} // namespace taxonomy } // namespace taxonomy
+5 -10
View File
@@ -313,7 +313,7 @@ namespace {
} }
bool compare(const loft& a, const loft& b) { 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) { 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) { piecewise_function::piecewise_function(double start, const spans_t& s, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) {
impl_ = new piecewise_function_impl(start, s, settings); 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) { 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, settings); impl_ = new piecewise_function_impl(start, pwfs);
}; };
piecewise_function::piecewise_function(const piecewise_function& other) : implicit_item(other) { 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::end() const { return impl_->end(); }
double piecewise_function::length() const { return impl_->length(); } 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) { ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) {
auto flat = make<taxonomy::collection>(); auto flat = make<taxonomy::collection>();
+3 -34
View File
@@ -347,8 +347,6 @@ typedef item const* ptr;
struct implicit_item : public geom_item { struct implicit_item : public geom_item {
DECLARE_PTR(implicit_item) DECLARE_PTR(implicit_item)
using geom_item::geom_item; using geom_item::geom_item;
virtual item::ptr evaluate() const = 0;
}; };
struct piecewise_function_impl; // forward declaration 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)>>>; 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 spans_t& s, 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 std::vector<piecewise_function::ptr>& pwfs, const IfcUtil::IfcBaseInterface* instance = nullptr);
piecewise_function(piecewise_function&&) = default; piecewise_function(piecewise_function&&) = default;
piecewise_function(const piecewise_function&); piecewise_function(const piecewise_function&);
virtual ~piecewise_function(); virtual ~piecewise_function();
const ifcopenshell::geometry::Settings* settings_ = nullptr;
const spans_t& spans() const; const spans_t& spans() const;
bool is_empty() const; bool is_empty() const;
double start() const; double start() const;
@@ -379,33 +375,6 @@ typedef item const* ptr;
return boost::hash<decltype(v)>{}(v); 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: private:
// note: it would be better if this were a std::unique_ptr, but that requires having the full definition // 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. // 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) DECLARE_PTR(loft)
item::ptr axis; item::ptr axis;
+2 -2
View File
@@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64)
PLATFORMTAG:=win_amd64 PLATFORMTAG:=win_amd64
endif endif
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-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-v0.7.11-03935a9-$(PLATFORM).zip IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(VERSION)-f5e02d1-$(PLATFORM).zip
.PHONY: test .PHONY: test
test: test:
@@ -104,12 +104,14 @@ class Usecase:
if self.settings["prop_name"]: if self.settings["prop_name"]:
self.quantities = set(self.settings["cost_item"].CostQuantities or []) self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for product in self.settings["products"]: 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"]) self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"])
if self.settings["prop_name"]: if self.settings["prop_name"]:
if ( if (
self.settings["cost_item"].CostQuantities self.settings["cost_item"].CostQuantities
and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower() and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower()
) or not product.is_a("IfcObject"): ):
continue continue
self.add_quantity_from_related_object(product) self.add_quantity_from_related_object(product)
if self.settings["prop_name"]: 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: for resource in resources:
cost, unit = ifcopenshell.util.resource.get_cost(resource) cost, unit = ifcopenshell.util.resource.get_cost(resource)
if not cost: if not cost:
cost, unit = ifcopenshell.util.resource.get_parent_cost( # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
resource parent_cost = 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. assert parent_cost
cost, unit = parent_cost
quantity = ifcopenshell.util.resource.get_quantity(resource) quantity = ifcopenshell.util.resource.get_quantity(resource)
if not cost or not quantity: if not cost or not quantity:
continue continue
@@ -42,6 +42,6 @@ def copy_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance)
inverses = file.get_inverse(profile) inverses = file.get_inverse(profile)
psets = [i for i in inverses if i.is_a("IfcProfileProperties")] psets = [i for i in inverses if i.is_a("IfcProfileProperties")]
for pset in psets: 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 new_pset.ProfileDefinition = new_profile
return new_profile return new_profile
@@ -167,6 +167,9 @@ def edit_pset(
class Usecase: class Usecase:
file: ifcopenshell.file
settings: dict[str, Any]
def execute(self) -> None: def execute(self) -> None:
self.update_pset_name() self.update_pset_name()
self.load_pset_template() self.load_pset_template()
@@ -378,7 +381,7 @@ class Usecase:
properties.append(self.file.create_entity("IfcPropertySingleValue", **args)) properties.append(self.file.create_entity("IfcPropertySingleValue", **args))
return properties 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"): if hasattr(self.settings["pset"], "HasProperties"):
self.settings["pset"].HasProperties = props self.settings["pset"].HasProperties = props
@@ -87,7 +87,8 @@ def unshare_pset(
for product in products: for product in products:
# No need to consider about profile/material properties since # No need to consider about profile/material properties since
# they are assigned to 1 element directly and therefore cannot be shared. # 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) pset_copies.append(pset_copy)
ifcopenshell.api.pset.assign_pset(file, [product], 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.""" """Retrieve the elements (or element types) that are using the provided property set."""
is_ifc2x3 = pset.file.schema == "IFC2X3" is_ifc2x3 = pset.file.schema == "IFC2X3"
elements = set() 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 rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
for rel in rels: for rel in rels:
elements.update(rel.RelatedObjects) elements.update(rel.RelatedObjects)
@@ -166,9 +166,9 @@ def get_quantity(resource: ifcopenshell.entity_instance) -> float:
return duration.total_seconds() / 3600 return duration.total_seconds() / 3600
def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[None, tuple[float, Union[str, None]]]: def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[tuple[float, Union[str, None]], None]:
if not resource.Nests: if not (nests := resource.Nests):
return return
else: else:
cost = get_cost(resource.Nests[0].RelatingObject) cost = get_cost(nests[0].RelatingObject)
return cost 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: if task.TaskTime:
current_date = ( current_date = (
ifcopenshell.util.date.ifc2datetime(getattr(task.TaskTime, attribute_name)) 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]: 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]: 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 """Gets the related products being output by a task
:param relating_product: One of the products already output by the 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 :param related_object: The IfcTask that you want to get all the related
products for. products for.
:type related_object: ifcopenshell.entity_instance, optional
:return: A set of IfcProducts output by the IfcTask. :return: A set of IfcProducts output by the IfcTask.
:rtype: set[ifcopenshell.entity_instance]
Example: Example:
@@ -478,17 +486,18 @@ def get_related_products(
products = ifcopenshell.util.sequence.get_related_products(related_object=task) 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() products = set()
related_object = None if not related_object and relating_product:
if related_object:
related_object = related_object
elif relating_product:
for reference in relating_product.ReferencedBy: for reference in relating_product.ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct"): if reference.is_a("IfcRelAssignsToProduct"):
related_object = reference.RelatedObjects[0] related_object = reference.RelatedObjects[0]
if related_object: if related_object:
assignments = related_object.HasAssignments assignments = related_object.HasAssignments
for assignment in assignments: for assignment in assignments:
if assignment.is_a("IfcRelAssignsToProduct"): if assignment.is_a("IfcRelAssignsToProduct"):
products.add(assignment.RelatingProduct.id()) products.add(assignment.RelatingProduct.id())
return products return products
@@ -368,6 +368,7 @@ class ShapeBuilder:
"ProfileType": profile_type, "ProfileType": profile_type,
"OuterCurve": outer_curve, "OuterCurve": outer_curve,
} }
if self.file.schema == "IFC2X3": if self.file.schema == "IFC2X3":
kwargs["Position"] = self.create_axis2_placement_2d() kwargs["Position"] = self.create_axis2_placement_2d()
+41 -2
View File
@@ -130,10 +130,48 @@ namespace {
if (aggregate_storage.which() == 0) { if (aggregate_storage.which() == 0) {
aggregate_storage = std::vector<std::decay_t<decltype(v)>>{ v }; aggregate_storage = std::vector<std::decay_t<decltype(v)>>{ v };
} else { } else {
auto* vec_ptr = boost::get<std::vector<std::decay_t<decltype(v)>>>(&aggregate_storage); if (auto* vec_ptr = boost::get<std::vector<std::decay_t<decltype(v)>>>(&aggregate_storage)) {
if (vec_ptr) {
vec_ptr->push_back(v); vec_ptr->push_back(v);
} else { } 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 // @todo would be cool if we can trace this back to file offset
auto current = boost::apply_visitor([](auto v) { auto current = boost::apply_visitor([](auto v) {
if constexpr (!std::is_same_v<decltype(v), Blank>) { if constexpr (!std::is_same_v<decltype(v), Blank>) {
@@ -145,6 +183,7 @@ namespace {
return std::string{}; return std::string{};
} }
}, aggregate_storage); }, aggregate_storage);
Logger::Error("Inconsistent aggregate valuation while attempting to append " + std::string(typeid(decltype(v)).name()) + " to an aggregate of " + current); Logger::Error("Inconsistent aggregate valuation while attempting to append " + std::string(typeid(decltype(v)).name()) + " to an aggregate of " + current);
// @todo boolean -> logical upgrade // @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); load(entity_instance_name, entity, context.push(), attribute_index == -1 ? (int) attribute_index_within_data : attribute_index);
} else { } else {
return_value++; 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); register_inverse(entity_instance_name, entity, next, attribute_index == -1 ? attribute_index_within_data : attribute_index);
} }
@@ -92,7 +92,7 @@ class Patcher:
) -> None: ) -> None:
geometry = getattr(shape, "geometry", shape) geometry = getattr(shape, "geometry", shape)
v = [[x.tolist() for x in ifcopenshell.util.shape.get_vertices(geometry)]] 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) replacements[element] = (v, f)
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products) 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. # Do the replacements outside the iterator to prevent messing up iterator state.
for element, geometry in replacements.items(): for element, geometry in replacements.items():
v, f = geometry v, f = geometry
if not v or not f:
continue
mesh = ifcopenshell.api.run( mesh = ifcopenshell.api.run(
"geometry.add_mesh_representation", "geometry.add_mesh_representation",
self.file, 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/Iterator.h"
%include "../ifcgeom/GeometrySerializer.h" %include "../ifcgeom/GeometrySerializer.h"
%include "../ifcgeom/taxonomy.h" %include "../ifcgeom/taxonomy.h"
%include "../ifcgeom/piecewise_function_evaluator.h"
%include "../serializers/SvgSerializer.h" %include "../serializers/SvgSerializer.h"
%include "../serializers/HdfSerializer.h" %include "../serializers/HdfSerializer.h"
@@ -315,7 +316,7 @@ assign_children_access(loop, edge);
assign_children_access(face, loop); assign_children_access(face, loop);
assign_children_access(shell, face); assign_children_access(shell, face);
assign_children_access(solid, shell); assign_children_access(solid, shell);
assign_children_access(loft, face); assign_children_access(loft, geom_item);
assign_children_access(boolean_result, geom_item); assign_children_access(boolean_result, geom_item);
%define assign_matrix_access(item_name) %define assign_matrix_access(item_name)
+2
View File
@@ -84,6 +84,7 @@
%{ %{
#include "../ifcgeom/Iterator.h" #include "../ifcgeom/Iterator.h"
#include "../ifcgeom/taxonomy.h" #include "../ifcgeom/taxonomy.h"
#include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE #ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/Serialization/Serialization.h" #include "../ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/kernels/opencascade/IfcGeomTree.h" #include "../ifcgeom/kernels/opencascade/IfcGeomTree.h"
@@ -159,6 +160,7 @@
%module ifcopenshell_wrapper %{ %module ifcopenshell_wrapper %{
#include "../ifcgeom/Converter.h" #include "../ifcgeom/Converter.h"
#include "../ifcgeom/taxonomy.h" #include "../ifcgeom/taxonomy.h"
#include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE #ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/Serialization/Serialization.h" #include "../ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/kernels/opencascade/IfcGeomTree.h" #include "../ifcgeom/kernels/opencascade/IfcGeomTree.h"