cost module web-ui:

- remove Blender operators in favor of ifc.api functions
- replace BonsaiBIM Data class dependency with new ifc5D2json script
- enable viewing multiple cost schedules
- add Identifaction column
ref #5369
This commit is contained in:
myoualid
2024-09-20 16:26:40 +01:00
parent e8f66abf17
commit e0e9892ff0
7 changed files with 455 additions and 296 deletions
@@ -19,6 +19,7 @@ body {
margin: 0;
padding: 0;
box-sizing: border-box;
height: 100vh;
}
.floating-form {
@@ -39,15 +40,15 @@ body {
.form-header {
display: flex;
justify-content: flex-start;
justify-content: center;
align-items: center;
margin-bottom: 10px;
background-color: #323936;
padding: var(--padding-medium);
font-size: large;
border-bottom: 1px solid #ddd;
border-radius: 6px 6px 0 0;
height: 5%;
position: relative;
}
.form-container {
@@ -58,9 +59,7 @@ body {
.form-section {
margin-bottom: 1em;
height: 65%;
overflow-y: auto;
overflow-x: hidden;
height: 70%;
}
.form-section h3 {
@@ -185,14 +184,21 @@ body {
.table-container {
margin-top: 1em;
overflow-x: auto;
overflow-y: auto;
position: relative;
border: 1px solid #ddd;
display: flex;
flex-direction: row;
}
.table-wrapper {
flex: 1;
overflow: auto;
border: 2px solid #254e3c;
/* shadow to the top */
box-shadow: 0 -5px 5px -0 #254e3c;
}
.floating-form .table-container {
max-height: 80%;
overflow: auto;
}
#cost-items {
@@ -208,9 +214,12 @@ body {
height: 50vh;
}
tr, td, th {
height: 100%;
}
table {
width: 100%;
height: 100%;
border-collapse: collapse;
user-select: none;
font-size: var(--fontSize);
@@ -247,8 +256,9 @@ td {
align-items: center;
}
#cost-items td:first-child {
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
}
@@ -375,10 +385,6 @@ form table {
[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 {
@@ -411,3 +417,7 @@ td {
position: absolute;
transform: translateY(-50%);
}
i {
margin-right: 5px;
}
@@ -222,7 +222,7 @@ button:hover {
border-collapse: collapse;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
flex-wrap: nowrap;
}
.column-container {
@@ -160,36 +160,6 @@ function handleConnectedClients(data) {
});
}
function handleCostValuesData(data) {
CostUI.createCostValuesForm({
costValues: data.data["cost_values"]["cost_values"],
costItemId: data.data["cost_values"]["cost_item_id"],
callbacks: {
editCostValues: editCostValues,
addCostValue: addCostValue,
deleteCostValue: deleteCostValue,
},
});
CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]);
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues,
});
}
function deleteCostItem(costItemId) {
executeOperator({ type: "deleteCostItem", costItemId: costItemId });
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -291,34 +261,6 @@ function toggleClientList() {
clientList.addClass("show");
}
function addCostItem(costItemId) {
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
function editCostItemName(costItemId, name) {
executeOperator({
type: "editCostItemName",
costItemId: costItemId,
name: name,
});
}
function selectAssignedElements(costItemId) {
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
}
function handleWebConnect() {
executeOperator({ type: "getPredefinedTypes" });
getCostSchedules();
}
function addSummaryCostItem(costScheduleId) {
executeOperator({
type: "addSummaryCostItem",
costScheduleId: costScheduleId,
});
}
function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const loadedSchedule = loadedSchedules[blenderId]
@@ -358,28 +300,20 @@ function handleCostSchedulesData(data) {
}
function handleCostItemsData(data) {
const costScheduleId = data.data["cost_items"]["schedule_data"][0]["id"];
const cards = document.querySelectorAll("[id^='schedule-']");
cards.forEach((card) => {
if (card.id === "schedule-" + data.data["cost_items"]["cost_schedule_id"]) {
card.classList.add("highlighted");
} else {
card.classList.remove("highlighted");
}
card.classList.remove("highlighted");
});
const costScheduleId = data.data["cost_items"]["cost_schedule_id"];
loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId);
loadedSchedules[data.blenderId] = costScheduleId;
const currency = data.data["cost_items"]["currency"]
? data.data["cost_items"]["currency"]["name"]
: "Undefined";
const costSchedule = data.data["cost_items"]["schedule_data"][0];
CostUI.createCostSchedule({
costItems: data.data["cost_items"]["cost_items"],
costSchedule: costSchedule,
currency: currency,
costScheduleId: costScheduleId,
blenderID: data.blenderId,
callbacks: {
addCostItem: addCostItem,
deleteCostItem: deleteCostItem,
@@ -394,23 +328,32 @@ function handleCostItemsData(data) {
});
}
function enableEditingQuantities(costItemId) {
executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
function addCostItem(costItemId) {
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
function duplicateCostItem(costItemId) {
executeOperator({ type: "duplicateCostItem", costItemId: costItemId });
function editCostItemName(costItemId, name) {
executeOperator({
type: "editCostItemName",
costItemId: costItemId,
name: name,
});
}
function executeOperator(operator, blenderId) {
const msg = {
sourcePage: "cost",
operator: operator,
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
function selectAssignedElements(costItemId) {
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
}
function handleWebConnect() {
executeOperator({ type: "getPredefinedTypes" });
getCostSchedules();
}
function addSummaryCostItem(costScheduleId) {
executeOperator({
type: "addSummaryCostItem",
costScheduleId: costScheduleId,
});
}
function loadCostSchedule(costScheduleId, blenderId) {
@@ -427,3 +370,53 @@ function getCostSchedules(blenderId) {
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId });
}
function enableEditingQuantities(costItemId) {
executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
}
function duplicateCostItem(costItemId) {
executeOperator({ type: "duplicateCostItem", costItemId: costItemId });
}
function handleCostValuesData(data) {
CostUI.createCostValuesForm({
costValues: data.data["cost_values"]["cost_values"],
costItemId: data.data["cost_values"]["cost_item_id"],
callbacks: {
editCostValues: editCostValues,
addCostValue: addCostValue,
deleteCostValue: deleteCostValue,
},
});
CostUI.highlightElement(data.data["cost_values"]["cost_item_id"]);
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues,
});
}
function deleteCostItem(costItemId) {
CostUI.deleteCostItem(costItemId);
executeOperator({ type: "deleteCostItem", costItemId: costItemId });
}
function executeOperator(operator, blenderId) {
const msg = {
sourcePage: "cost",
operator: operator,
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
}
@@ -7,17 +7,42 @@ export class CostUI {
}
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
const table = document.getElementById("cost-items-" + id);
table ? table.parentElement.remove() : null;
}
static createCostTable(id, currency, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
static createCostTable({ costSchedule, currency, callbacks }) {
let tableWrapper;
const id = costSchedule.id;
if (CostUI.isCostScheduleLoaded(id)) {
const table = document.getElementById("cost-items-" + id);
tableWrapper = table.parentElement;
table.remove();
} else {
tableWrapper = document.createElement("div");
tableWrapper.classList.add("table-wrapper");
document.getElementById("cost-items").appendChild(tableWrapper);
const tableHeader = document.createElement("div");
tableHeader.classList.add("form-header");
const text = CostUI.Text(
costSchedule.Name,
"fa-solid fa-money-bill-wave",
"x-large"
);
const callback = () => {
tableWrapper.remove();
CostUI.unhighlightElement("schedule-" + id);
};
let closeButton = CostUI.createCloseButton(callback);
tableHeader.appendChild(text);
tableHeader.appendChild(closeButton);
tableWrapper.appendChild(tableHeader);
}
const table = document.createElement("table");
table.id = "cost-items-" + id;
const tbody = document.createElement("tbody");
tbody.setAttribute("id", "cost-items");
const columnHeaders = [
"ID",
"Name",
"Quantity",
"Unit",
@@ -44,7 +69,7 @@ export class CostUI {
table.appendChild(thead);
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
tableWrapper.appendChild(table);
CostUI.createContextMenu(callbacks);
table.get_blender_id = function () {
@@ -54,12 +79,11 @@ export class CostUI {
return [table, tbody];
}
static deleteCostItem(costItemId, callback) {
static deleteCostItem(costItemId) {
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) {
@@ -79,16 +103,20 @@ export class CostUI {
event.preventDefault();
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
if (targetRow) {
const isCostTable = targetRow.parentElement.parentElement
? targetRow.parentElement.parentElement.id.includes("cost-items")
: false;
if (isCostTable) {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
}
});
@@ -102,104 +130,79 @@ export class CostUI {
const editCostValuesButton = document.getElementById(
"edit-cost-values-button"
);
editCostValuesButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
document.getElementById("context-menu").style.display = "none";
});
const addButton = document.getElementById("add-cost-item-button");
if (!addButton.dataset.listenerAdded) {
function addCostItemHandler(e) {
e.stopPropagation();
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null;
}
document.getElementById("context-menu").style.display = "none";
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
}
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"));
CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
}
document.getElementById("context-menu").style.display = "none";
});
deleteCostItemButton.hasListener = true;
}
const duplicateButton = document.getElementById("duplicate-button");
if (duplicateButton && !duplicateButton.hasListener) {
duplicateButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.duplicateCostItem(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
duplicateButton.hasListener = true;
}
const getSelectedProducts = document.getElementById("assign-selection");
if (getSelectedProducts && !getSelectedProducts.hasListener) {
getSelectedProducts.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingQuantities(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
getSelectedProducts.hasListener = true;
function handleButtonClick(button, callback) {
if (button && !button.hasListener) {
button.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callback(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
button.hasListener = true;
}
}
function getColumnNames(tableId) {
const table = document.getElementById(tableId);
const headerRow = table.querySelector("thead tr");
return Array.from(headerRow.children).map((headerCell) =>
headerCell.textContent.trim()
);
// if (deleteCostItemButton && !deleteCostItemButton.hasListener) {
// deleteCostItemButton.addEventListener("click", function () {
// const targetRow = document.getElementById("context-menu").targetRow;
// if (targetRow) {
// const costItemId = parseInt(targetRow.getAttribute("id"));
// CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
// }
// document.getElementById("context-menu").style.display = "none";
// });
// deleteCostItemButton.hasListener = true;
// }
handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem);
handleButtonClick(duplicateButton, callbacks.duplicateCostItem);
handleButtonClick(getSelectedProducts, callbacks.enableEditingQuantities);
handleButtonClick(editCostValuesButton, callbacks.enableEditingCostValues);
handleButtonClick(addButton, callbacks.addCostItem);
handleButtonClick(deleteCostItemButton, callbacks.deleteCostItem);
addTableListeners(callbacks);
function addTableListeners(callbacks) {
function getColumnNames(tableId) {
const table = document.getElementById(tableId);
const headerRow = table.querySelector("thead tr");
return Array.from(headerRow.children).map((headerCell) =>
headerCell.textContent.trim()
);
}
document
.getElementById("cost-items")
.addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
const columnIndex = Array.from(targetRow.children).indexOf(
targetCell
);
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex];
if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
}
});
}
document
.getElementById("cost-items")
.addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
const columnIndex = Array.from(targetRow.children).indexOf(
targetCell
);
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex];
if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
if (columnName === "Quantity") {
callbacks.enableEditingQuantities
? callbacks.enableEditingQuantities(costItemId)
: null;
}
}
});
}
static deleteCostItemRow(targetRow, expandedState) {
@@ -353,19 +356,13 @@ export class CostUI {
return div;
}
static createCostSchedule({
costItems,
currency,
costScheduleId,
blenderID,
callbacks = {},
}) {
const [table, tbody] = CostUI.createCostTable(
blenderID,
static createCostSchedule({ costSchedule, currency, callbacks = {} }) {
const [table, tbody] = CostUI.createCostTable({
costSchedule,
currency,
callbacks
);
if (costItems.length === 0) {
callbacks,
});
if (costSchedule["cost_items"].length === 0) {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.colSpan = 6;
@@ -383,7 +380,13 @@ export class CostUI {
td.appendChild(addSummaryCostItemButton);
addSummaryCostItemButton.classList.add("action-button");
} else {
CostUI.createCostTree(costItems, tbody, 0, null, callbacks);
CostUI.createCostTree(
costSchedule["cost_items"],
tbody,
0,
null,
callbacks
);
CostUI.applyExpandedState();
}
}
@@ -404,9 +407,9 @@ export class CostUI {
);
container.appendChild(row);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
if (costItem.IsNestedBy && costItem.IsNestedBy.length > 0) {
CostUI.createCostTree(
costItem.is_nested_by,
costItem.IsNestedBy,
container,
nestingLevel + 1,
costItem.id,
@@ -434,6 +437,10 @@ export class CostUI {
? CostUI.format_number(costItem.TotalAppliedValue)
: "-";
const row = CostUI.createCostItemRow(costItem, nestingLevel, parentID);
const identification = costItem.Identification
? costItem.Identification
: "XXX";
const idCell = CostUI.createTableCell(identification);
const expandButton = CostUI.createExpandButton(costItem);
const nameCell = CostUI.createNameCell(
costItem,
@@ -456,6 +463,7 @@ export class CostUI {
actionsCell.classList.add("actions-column");
row.appendChild(idCell);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
@@ -491,7 +499,7 @@ export class CostUI {
expandButton.classList.add("fa-solid");
expandButton.classList.add("fa-angle-right");
if (row.is_nested_by && row.is_nested_by.length > 0) {
if (row.IsNestedBy && row.IsNestedBy.length > 0) {
expandButton.classList.add("fa-angle-right");
} else {
expandButton.style.visibility = "hidden";
@@ -614,7 +622,7 @@ export class CostUI {
static createNameCell(costItem, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = costItem.name ? costItem.name : "Unnamed";
nameInput.value = costItem.Name ? costItem.Name : "Unnamed";
nameInput.addEventListener("change", function () {
callbacks.editCostItemName
@@ -631,6 +639,7 @@ export class CostUI {
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.classList.add("row-container");
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
const widthButton = 20;
@@ -652,7 +661,7 @@ export class CostUI {
const totalCostCell = document.createElement("td");
const totalCost = CostUI.format_number(costItem.TotalCost);
totalCostCell.textContent = totalCost;
if (costItem.is_sum) {
if (costItem.IsSum) {
totalCostCell.textContent = totalCost + " *";
} else if (!costItem.TotalCost) {
const button = CostUI.createButton("Sum", "fa-solid fa-calculator");
@@ -746,7 +755,7 @@ export class CostUI {
const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash");
deleteButton.addEventListener(
"click",
CostUI.deleteCostItem.bind(null, costItemId, deleteCostItem)
deleteCostItem.bind(null, costItemId)
);
return deleteButton;
}
@@ -1089,6 +1098,13 @@ export class CostUI {
}
static Form({ id, name, icon = "fa-solid fa-file", shouldHide = false }) {
function style(form) {
form.classList.add("floating-form");
form.style.position = "absolute";
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
}
if (document.getElementById(id)) {
const formContainer = document
.getElementById(id)
@@ -1098,17 +1114,19 @@ export class CostUI {
}
const form = document.createElement("form");
form.id = id;
form.classList.add("floating-form");
form.style.position = "absolute";
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
style(form);
const header = CostUI.createHeader(name, icon);
let closeButton;
if (shouldHide) {
closeButton = CostUI.createHideButton(form);
} else {
closeButton = CostUI.createCloseButton(form);
const callback = () => {
form.remove();
const costItemId = form.id.split("-").pop();
CostUI.unhighlightElement(costItemId);
};
closeButton = CostUI.createCloseButton(callback); // Pass the function reference without invoking it
}
form.appendChild(header);
form.appendChild(closeButton);
@@ -1200,16 +1218,11 @@ export class CostUI {
return hideButton;
}
static createCloseButton(form) {
static createCloseButton(callback) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "×";
closeButton.addEventListener("click", function () {
form.remove();
const costItemId = form.id.split("-").pop();
CostUI.unhighlightElement(costItemId);
});
closeButton.addEventListener("click", callback);
return closeButton;
}
@@ -2114,9 +2127,7 @@ export class CostUI {
});
}
});
}
else if (key.toLowerCase().includes("value")) {
} else if (key.toLowerCase().includes("value")) {
const cell = document.createElement("td");
const input = document.createElement("input");
input.type = "number";
+1 -6
View File
@@ -865,12 +865,7 @@ class Cost(bonsai.core.tool.Cost):
def generate_cost_schedule_browser(cls, cost_chedule) -> None:
if not bpy.context.scene.WebProperties.is_connected:
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(), "currency": cls.currency()},
data_key="cost_items",
event="cost_items",
)
tool.Web.load_cost_schedule_web_ui(cost_chedule)
@classmethod
def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict:
+36 -46
View File
@@ -41,6 +41,7 @@ from time import sleep
from pathlib import Path
import bonsai.core.sequence
import bonsai.core.cost
from ifc5d.ifc2json import ifc5D2json
sio = None
ws_process = None
@@ -346,9 +347,11 @@ class Web(bonsai.core.tool.Web):
ifc_file = tool.Ifc.get()
if operator_data["type"] == "getPredefinedTypes":
print("getting predefined types")
predefined_types = ifcopenshell.util.cost.get_cost_schedule_types(ifc_file)
cls.send_webui_data(data=predefined_types, data_key="predefined_types", event="predefined_types")
cls.send_webui_data(
data=ifcopenshell.util.cost.get_cost_schedule_types(ifc_file),
data_key="predefined_types",
event="predefined_types"
)
if operator_data["type"] == "addCostSchedule":
bonsai.core.cost.add_cost_schedule(
tool.Ifc, name=operator_data["name"], predefined_type=operator_data["predefinedType"]
@@ -361,7 +364,6 @@ class Web(bonsai.core.tool.Web):
event="cost_schedules",
)
if operator_data["type"] == "getCostSchedules":
print("getting cost schedules")
cost_schedules_json = [cs.get_info(recursive=True) for cs in ifc_file.by_type("IfcCostSchedule")]
currency = tool.Cost.currency()
cls.send_webui_data(
@@ -370,38 +372,36 @@ class Web(bonsai.core.tool.Web):
event="cost_schedules",
)
if operator_data["type"] == "loadCostSchedule":
print("loading cost schedule")
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "selectAssignedElements":
print("selecting assigned elements")
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"] == "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)
tool.Ifc.run("cost.add_cost_item", cost_schedule=cost_schedule)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addCostItem":
if not operator_data["costItemId"]:
return
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
cost_item=tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run("cost.add_cost_item", cost_item=cost_item)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "deleteCostItem":
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
bpy.ops.bim.remove_cost_item(cost_item=operator_data["costItemId"])
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run("cost.remove_cost_item", cost_item=cost_item)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "duplicateCostItem":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
new_cost_item = ifcopenshell.api.cost.copy_cost_item(ifc_file, cost_item)
tool.Ifc.run("cost.copy_cost_item", cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run("cost.edit_cost_item", cost_item=cost_item, attributes={"Name": operator_data["name"]})
tool.Cost.load_cost_schedule_tree()
if operator_data["type"] == "enableEditingCostValues":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
cost_values = ifcopenshell.util.cost.get_cost_values(cost_item)
@@ -422,18 +422,15 @@ class Web(bonsai.core.tool.Web):
)
if operator_data["type"] == "addSumCostValue":
cost_item = ifc_file.by_id(operator_data["costItemId"])
value = ifcopenshell.api.cost.add_cost_value(
ifc_file,
parent=cost_item,
)
ifcopenshell.api.cost.edit_cost_value(file=ifc_file, cost_value=value, attributes={"Category": "*"})
value = tool.Ifc.run("cost.add_cost_value", parent=cost_item)
tool.Ifc.run("cost.edit_cost_value", cost_value=value, attributes={"Category": "*"})
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
tool.Cost.load_cost_schedule_tree()
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "deleteCostValue":
bpy.ops.bim.remove_cost_value(parent=operator_data["costItemId"], cost_value=operator_data["costValueId"])
cost_item = ifc_file.by_id(operator_data["costItemId"])
cost_value = ifc_file.by_id(operator_data["costValueId"])
tool.Ifc.run("cost.remove_cost_value", parent=cost_item, cost_value=cost_value)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostValues":
@@ -441,7 +438,6 @@ class Web(bonsai.core.tool.Web):
cost_item = ifc_file.by_id(cost_item_id)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
for value_data in operator_data["costValues"] or []:
print("editing cost value data", value_data)
value = ifc_file.by_id(value_data["id"])
if value_data["costType"] == "FIXED":
attributes = {"AppliedValue": value_data["appliedValue"], "Category": None}
@@ -453,51 +449,43 @@ class Web(bonsai.core.tool.Web):
elif value_data["costType"] == "SUM":
attributes = {"Category": "*"}
ifcopenshell.api.cost.edit_cost_value(file=ifc_file, cost_value=value, attributes=attributes)
print(value.get_info())
tool.Cost.load_cost_schedule_tree()
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addProductAssignments":
prop_name = operator_data["propName"]
if prop_name == "count":
prop_name = ""
bpy.ops.bim.assign_cost_item_quantity(
cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name
)
bonsai.core.cost.assign_cost_item_quantity(tool.Ifc, tool.Cost, cost_item=ifc_file.by_id(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)
cls.load_cost_item_quantities_ui(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"])
tool.Ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=operator_data["ifcClass"])
cls.load_cost_schedule_web_ui(cost_schedule)
cls.enableEditingCostItemQuantities(cost_item)
cls.load_cost_item_quantities_ui(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"]))
tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=operator_data["attributes"])
cls.load_cost_schedule_web_ui(cost_schedule)
cls.enableEditingCostItemQuantities(cost_item)
cls.load_cost_item_quantities_ui(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_item = ifc_file.by_id(operator_data["costItemId"])
physical_quantity = ifc_file.by_id(operator_data["quantityId"])
tool.Ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity)
cost_schedule = tool.Cost.get_cost_schedule(cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
cls.enableEditingCostItemQuantities(cost_item)
cls.load_cost_item_quantities_ui(cost_item)
@classmethod
def enableEditingCostItemQuantities(cls, cost_item):
def load_cost_item_quantities_ui(cls, cost_item : ifcopenshell.entity_instance) -> None:
if not cost_item:
return
selected_products = list(tool.Spatial.get_selected_products())
@@ -520,7 +508,7 @@ class Web(bonsai.core.tool.Web):
)
@classmethod
def selection_data(cls, cost_item, elements):
def selection_data(cls, cost_item : ifcopenshell.entity_instance, elements : list) -> list:
if not elements:
return []
if not cost_item:
@@ -566,13 +554,15 @@ class Web(bonsai.core.tool.Web):
return data
@classmethod
def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
def load_cost_schedule_web_ui(cls, cost_schedule :ifcopenshell.entity_instance) -> None:
ifc2json = ifc5D2json()
ifc2json.file_path = tool.Ifc.get()
ifc2json.cost_schedule = cost_schedule
schedule_data = ifc2json.convert_ifc_to_json()
cls.send_webui_data(
data={
"cost_items": json_data,
"cost_schedule_id": cost_schedule.id(),
"currency": tool.Cost.currency(),
"schedule_data": schedule_data,
},
data_key="cost_items",
event="cost_items",
+160
View File
@@ -0,0 +1,160 @@
import ifcopenshell
import ifcopenshell.util.unit
from typing import Any
import ifcopenshell.util.cost
import ifcopenshell.util.date
CostItem = dict[str, Any]
class ifc5D2json:
def __init__(self):
self.json: str = None
self.file_path: str = None
self.file: ifcopenshell.file = None
self.cost_schedule: ifcopenshell.entity_instance = None
self.units: dict[str, ifcopenshell.entity_instance] = {}
self._cost_values = {}
self.data: list[dict[str, Any]] = []
def convert_ifc_to_json(self):
self.data = []
if not isinstance(self.file_path, str):
self.file = self.file_path
else:
self.file = ifcopenshell.open(self.file_path)
if not self.cost_schedule:
self.extract_cost_schedules()
else:
self.extract_cost_schedule()
return self.data
def extract_cost_schedule(self):
data = self.cost_schedule.get_info(recursive=True)
data["cost_items"] = self.build_schedule_tree(self.cost_schedule)
self.data.append(data)
def extract_cost_schedules(self):
for cost_schedule in self.file.by_type("IfcCostSchedule"):
data = cost_schedule.get_info(recursive=True)
data["cost_items"] = self.build_schedule_tree(cost_schedule)
self.data.append(data)
def build_schedule_tree(self, cost_schedule: ifcopenshell.entity_instance) -> dict[str, Any]:
schedule_data = []
self._cost_values = {}
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects:
self.extract_cost_item_data(cost_item, schedule_data)
return schedule_data
def extract_cost_item_data(self , cost_item: ifcopenshell.entity_instance, json_data):
data = {}
self.extract_cost_item_quantities(cost_item, data)
self.populate_cost_values(cost_item, data)
self.populate_nesting_index(cost_item, data)
data = {**data, **cost_item.get_info(recursive=True)}
data["id"] = cost_item.id()
data["IsNestedBy"] = []
data["IsSum"] = self.check_if_cost_item_is_sum(cost_item)
json_data.append(data)
for rel in cost_item.IsNestedBy or []:
for sub_cost in rel.RelatedObjects:
self.extract_cost_item_data(sub_cost, data["IsNestedBy"])
def check_if_cost_item_is_sum(self, cost_item: ifcopenshell.entity_instance) -> bool:
cost_values = []
if cost_item.is_a("IfcCostItem"):
cost_values = cost_item.CostValues
elif cost_item.is_a("IfcCostValue"):
cost_values = cost_item.Components
for cost_value in cost_values or []:
if cost_value.Category == "*":
return True
return False
def populate_nesting_index(self, cost_item, data):
data["NestingIndex"] = None
for rel in cost_item.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(cost_item)
def populate_cost_values(self, root_element, data):
# data["CostValues"] = []
data["CategoryValues"] = {}
data["UnitBasisValueComponent"] = None
data["UnitBasisUnitSymbol"] = None
data["TotalAppliedValue"] = 0.0
data["TotalCost"] = 0.0
has_unit_basis = False
is_sum = False
if root_element.is_a("IfcCostItem"):
values = root_element.CostValues
elif root_element.is_a("IfcConstructionResource"):
values = root_element.BaseCosts
for cost_value in values or []:
self.extract_cost_value(root_element, data, cost_value)
# data["CostValues"].append(cost_value.id())
data["TotalAppliedValue"] += self._cost_values[cost_value.id()]["AppliedValue"]
if cost_value.UnitBasis:
cost_value_data = self._cost_values[cost_value.id()]
data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"]
data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"]
has_unit_basis = True
else:
data["UnitBasisValueComponent"] = 1
data["UnitBasisUnitSymbol"] = "U"
if cost_value.Category == "*":
is_sum = True
cost_quantity = 1 if data["TotalCostQuantity"] is None else data["TotalCostQuantity"]
if has_unit_basis:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity / data["UnitBasisValueComponent"]
else:
data["TotalCost"] = data["TotalAppliedValue"] * cost_quantity
if is_sum:
data["TotalAppliedValue"] = None
def extract_cost_value(self, root_element, root_element_data, cost_value):
value_data = cost_value.get_info()
del value_data["AppliedValue"]
if value_data["UnitBasis"]:
data = cost_value.UnitBasis.get_info()
data["ValueComponent"] = data["ValueComponent"].wrappedValue
data["UnitComponent"] = data["UnitComponent"].id()
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(cost_value.UnitBasis.UnitComponent)
value_data["UnitBasis"] = data
if value_data["ApplicableDate"]:
value_data["ApplicableDate"] = ifcopenshell.util.date.ifc2datetime(value_data["ApplicableDate"])
if value_data["FixedUntilDate"]:
value_data["FixedUntilDate"] = ifcopenshell.util.date.ifc2datetime(value_data["FixedUntilDate"])
value_data["Components"] = [c.id() for c in value_data["Components"] or []]
value_data["AppliedValue"] = ifcopenshell.util.cost.calculate_applied_value(root_element, cost_value)
if cost_value.Category not in [None, "*"]:
root_element_data["CategoryValues"].setdefault(cost_value.Category, 0)
root_element_data["CategoryValues"][cost_value.Category] += value_data["AppliedValue"]
value_data["Formula"] = ifcopenshell.util.cost.serialise_cost_value(cost_value)
self._cost_values[cost_value.id()] = value_data
for component in cost_value.Components or []:
self.extract_cost_value(root_element, root_element_data, component)
def extract_cost_item_quantities(self, cost_item, data):
data["TotalCostQuantity"] = ifcopenshell.util.cost.get_total_quantity(cost_item)
data["UnitSymbol"] = "-"
if cost_item.CostQuantities:
quantity = cost_item.CostQuantities[0]
data["QuantityType"] = quantity.is_a()
unit = ifcopenshell.util.unit.get_property_unit(quantity, self.file)
if unit:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
if quantity.is_a("IfcPhysicalSimpleQuantity"):
measure_class = (
quantity.wrapped_data.declaration()
.as_entity()
.attribute_by_index(3)
.type_of_attribute()
.declared_type()
.name()
)
if "Count" in measure_class:
data["UnitSymbol"] = "U"