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