Merge remote-tracking branch 'origin/v0.8.0' into light/radiance

This commit is contained in:
Chirag Singh
2024-09-01 13:52:50 +05:30
49 changed files with 1172 additions and 320 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=7e6607a
OLD:=03935a9
.PHONY: bump
bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
@@ -166,6 +166,16 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_values(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_values"] = data
await sio.emit("cost_values", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_value(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_value"] = data
await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
@@ -0,0 +1,89 @@
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: rgb(51, 51, 51);
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
color: white;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
color: black;
}
/* Context menu button is clicked, should zoom into object and change color */
.context-menu button:active {
background-color: #ff9634;
color: rgba(37, 51, 77, 0.466);
/* zoom */
transform: scale(0.9);
}
#cost-items tr:hover {
background-color: #f0f0f0;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute; /* Set initial position to absolute */
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move; /* Change cursor to move */
}
.cost-values-table {
width: 100%;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #f2f2f2;
text-align: left;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
@@ -1,4 +1,5 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
:root {
--font-family: Arial, sans-serif;
@@ -23,6 +23,8 @@ function connectSocket() {
socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
}
function handleBlenderConnect(blenderId) {
@@ -46,8 +48,20 @@ function handleBlenderDisconnect(blenderId) {
});
}
function removeTableElement(blenderId) {
$("#cost-items-" + blenderId).remove();
}
function handleCostValueData(data) {
const costItemId = data.data["cost_value"]["cost_item_id"];
const costValueId = data.data["cost_value"]["cost_value_id"];
console.log("Handling cost value data", costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId);
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
@@ -56,6 +70,29 @@ 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,
},
});
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues
});
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -99,7 +136,6 @@ function setTheme(theme) {
}
function addCostItem(costItemId) {
console.log("addCostItem", costItemId);
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
@@ -119,11 +155,7 @@ function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
const currency = data.data["cost_schedules"]["currency"]["name"];
console.log(data.data["cost_schedules"]);
const costScheduleDiv = $("#cost-schedules");
costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
@@ -136,7 +168,6 @@ function handleCostSchedulesData(data) {
}
function handleCostItemsData(data) {
console.log(data);
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
@@ -144,6 +175,7 @@ function handleCostItemsData(data) {
"addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
}
});
}
@@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) {
function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId);
}
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
@@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -115,7 +114,6 @@ function handleThemeData(themeData) {
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#gantt-stylesheet")[0];
if (styleElement) {
@@ -133,7 +131,6 @@ function handleWorkScheduleData(data) {
const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => {
console.log(workSchedule);
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
@@ -142,11 +139,8 @@ function handleWorkScheduleData(data) {
}
function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
@@ -183,7 +177,6 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log('default data',data);
}
// Function to add a new gantt with data and filename
@@ -1,14 +1,6 @@
export class CostUI {
constructor() {}
createButton() {
console.log("Button created");
}
createInput() {
console.log("Input created");
}
static isCostScheduleLoaded(id) {
const existingTable = document.getElementById('cost-items-' + id);
return existingTable !== null;
@@ -17,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id) {
static createTable(id, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
@@ -51,7 +43,7 @@ export class CostUI {
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu();
CostUI.createContextMenu(callbacks);
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
@@ -71,52 +63,19 @@ export class CostUI {
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
}
#cost-items tr:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
`;
document.head.appendChild(style);
}
static createContextMenu() {
static createContextMenu(callbacks) {
// Create context menu
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
contextMenu.innerHTML = `
<button id="edit-button">Edit</button>
<button id="add-cost-item-button">Add sub-cost</button>
<button id="edit-cost-values-button">Edit</button>
<button id="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
`;
@@ -126,6 +85,7 @@ export class CostUI {
document.addEventListener("contextmenu", function(event) {
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";
@@ -134,6 +94,7 @@ export class CostUI {
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
@@ -146,14 +107,32 @@ export class CostUI {
}
});
document.getElementById("edit-button").addEventListener("click", function() {
const editCostValuesButton = document.getElementById("edit-cost-values-button");
editCostValuesButton.addEventListener("click", function() {
console.log("Edit cost values executed");
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
// Implement your edit action here
console.log("Edit row:", targetRow.getAttribute("id"));
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null;
}
});
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";
}
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
@@ -246,7 +225,7 @@ export class CostUI {
}
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
const [table, tbody] = CostUI.createTable(blenderID);
const [table, tbody] = CostUI.createTable(blenderID, callbacks);
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
@@ -263,6 +242,35 @@ export class CostUI {
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = CostUI.createTableRow(obj, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(obj);
const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity);
const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(obj);
const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
}
static createTableRow(obj, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID);
@@ -270,6 +278,10 @@ export class CostUI {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
return row;
}
static createExpandButton(obj) {
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
@@ -277,51 +289,62 @@ export class CostUI {
} else {
expandButton.style.visibility = "hidden";
}
//row.appendChild(expandButton);
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
});
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
});
nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
}
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
row.appendChild(nameCell);
const totalCostQuantityCell = document.createElement("td");
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
row.appendChild(totalCostQuantityCell);
const unitSymbolCell = document.createElement("td");
unitSymbolCell.textContent = obj.UnitSymbol;
row.appendChild(unitSymbolCell);
const totalAppliedValueCell = document.createElement("td");
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
row.appendChild(totalAppliedValueCell);
return nameCell;
}
static createTableCell(content) {
const cell = document.createElement("td");
cell.textContent = content;
return cell;
}
static createTotalCostCell(obj) {
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
row.appendChild(totalCostCell);
return totalCostCell;
}
static createFlexContainerCell(obj, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = CostUI.createAddButton(obj, callbacks);
const selectButton = CostUI.createSelectButton(obj, callbacks);
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
return flexContainerCell;
}
static createAddButton(obj, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
@@ -329,31 +352,17 @@ export class CostUI {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
});
return addButton;
}
static createSelectButton(obj, callbacks) {
const selectButton = document.createElement("button");
selectButton.textContent = "Select";
selectButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
});
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
return selectButton;
}
static hideNestedRows(parentId) {
@@ -461,4 +470,267 @@ export class CostUI {
return card;
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
// Check if the form already exists and remove it if it does
const formId = "cost-values-form-" + costItemId;
let existingForm = document.getElementById(formId);
if (existingForm) {
existingForm.remove();
}
const form = CostUI.createFormElement(formId, "cost-values-form");
const header = CostUI.createHeader("Cost Values Form");
form.appendChild(header);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]);
costValues.forEach(costValue => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue);
table.appendChild(tr);
});
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(addButton);
const submitButton = CostUI.createSubmitButton(callbacks);
form.appendChild(submitButton);
const closeButton = CostUI.createCloseButton(form);
form.appendChild(closeButton);
CostUI.makeHeaderDraggable(header, form);
document.body.appendChild(form);
return form;
}
static getCostValuesTable(costItemId) {
return document.getElementById("cost-values-table-" + costItemId);
}
static addNewCostValueRow(costItemId, costValueId) {
const table = CostUI.getCostValuesTable(costItemId);
// check if table exists
if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
console.log("Adding new cost value row", costValueId);
console.log(table)
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId});
table.appendChild(tr);
}
static createCostValuesTable(costItemId, headers) {
const table = document.createElement("table");
table.classList.add("cost-values-table");
table.id = "cost-values-table-" + costItemId;
const headerRow = document.createElement("tr");
headers.forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
headerRow.appendChild(th);
});
table.appendChild(headerRow);
return table;
}
static createCostvaluesRow(costItemId,costValue) {
function cleanLabel(value) {
// remove anything which is not a dot or a digit
return value.replace(/[^0-9.]/g, '');
}
if (document.getElementById(costValue.id)) {
return document.getElementById(costValue.id);
}
const tr = document.createElement("tr");
tr.isCostValue = true;
tr.parent = costItemId;
tr.id = costValue.id;
let costType = "FIXED";
if (costValue.category === "*") {
costType = "SUM";
}
else if (costValue.category && costValue.category !== "*") {
costType = "CATEGORY";
}
else if (costValue.applied_value) {
costType = "FIXED";
}
const typeCell = CostUI.createTableDropdown("type", costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function() {
const selectedType = this.value;
CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1);
});
tr.appendChild(typeCell);
const categoryCell = CostUI.createTableInput("text", "category", costValue.category);
tr.appendChild(categoryCell);
let value
if (costValue.category === "*"){
value = cleanLabel(costValue.label)
}
else {
value = costValue.applied_value
}
const valueCell1 = CostUI.createTableInput("number", "value", value);
tr.appendChild(valueCell1);
// Apply initial state based on costType
CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1);
return tr;
}
static updateRowBasedOnType(type, categoryCell, valueCell1) {
if (type === "FIXED") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "CATEGORY") {
categoryCell.querySelector("input").disabled = false;
categoryCell.querySelector("input").style.backgroundColor = "";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "SUM") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = true;
valueCell1.querySelector("input").style.backgroundColor = "lightgrey";
}
}
static createTableDropdown(name, 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;
option.textContent = optionValue;
if (optionValue === value) {
option.selected = true;
}
dropdown.appendChild(option);
});
cell.appendChild(dropdown);
return cell;
}
static createTableInput(type, name, value) {
const cell = document.createElement("td");
const input = document.createElement("input");
input.type = type;
input.name = name;
input.value = value;
cell.appendChild(input);
return cell;
}
static createAddCostValueButton(costItemId, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.preventDefault();
callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null;
});
return addButton;
}
static createSubmitButton(callbacks) {
const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Save";
submitButton.addEventListener("click", function(e) {
e.preventDefault();
const form = this.closest("form");
const costValues = [];
const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue);
rows.forEach(row => {
const costCategory = row.querySelector("input[name='category']").value;
const appliedValue = parseFloat(row.querySelector("input[name='value']").value);
const costType = row.querySelector("select[name='type']").value;
const id = parseInt(row.id);
costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent });
});
const costItemId = parseInt(form.id.split("-")[3]);
console.log(form.id)
console.log("Cost values to be saved:", costItemId);
callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null;
form.remove();
});
return submitButton;
}
static createFormElement(id, className) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute"; // Set initial position to absolute
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
return form;
}
static createHeader(text) {
const header = document.createElement("div");
header.classList.add("form-header");
header.textContent = text;
return header;
}
static createCloseButton(form) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function() {
form.remove();
});
return closeButton;
}
static makeHeaderDraggable(header, form) {
header.addEventListener("mousedown", function(e) {
let offsetX = e.clientX - form.getBoundingClientRect().left;
let offsetY = e.clientY - form.getBoundingClientRect().top;
function mouseMoveHandler(e) {
form.style.left = `${e.clientX - offsetX}px`;
form.style.top = `${e.clientY - offsetY}px`;
}
function mouseUpHandler() {
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
}
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
});
}
}
@@ -4,8 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
<link rel="stylesheet" href="/static/css/components/card.css" />
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
@@ -79,6 +79,7 @@ classes = (
operator.SelectUnassignedProducts,
operator.UnassignCostItemQuantity,
operator.UnassignCostItemType,
operator.GenerateCostScheduleBrowser,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
+2 -8
View File
@@ -265,16 +265,10 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return results
cost_item = tool.Ifc.get().by_id(ifc_id)
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
return results
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls):
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.add_currency(tool.Ifc, tool.Cost)
class GenerateCostScheduleBrowser(bpy.types.Operator):
bl_idname = "bim.generate_cost_schedule_browser"
bl_label = "Generate Cost Schedule Browser"
bl_options = {"REGISTER", "UNDO"}
cost_schedule: bpy.props.IntProperty()
def execute(self, context):
core.generate_cost_schedule_browser(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
return {"FINISHED"}
+2 -1
View File
@@ -73,7 +73,8 @@ class BIM_PT_cost_schedules(Panel):
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.export_cost_schedules", text="Export spreadsheet", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.generate_cost_schedule_browser", text="Generate spreadsheet browsser", icon="URL").cost_schedule = cost_schedule["id"]
row2 = col.row(align=True)
row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
@@ -204,6 +204,8 @@ def format_distance(
tx_dist = ""
if feet is not None:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
if feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
+202 -14
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import bpy
import json
import time
@@ -43,13 +42,12 @@ import bonsai.core.drawing as core
import bonsai.bim.module.drawing.svgwriter as svgwriter
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.module.drawing.scheduler as scheduler
import bonsai.bim.module.drawing.helper as helper
import bonsai.bim.export_ifc
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
@@ -257,6 +255,7 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
self.svg_writer.calculate_scale()
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
@@ -269,7 +268,12 @@ class CreateDrawing(bpy.types.Operator):
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
linework_svg = self.generate_linework(context)
if self.camera.data.BIMCameraProperties.linework_mode == "OPENCASCADE":
linework_svg = self.generate_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
with profile("Generate annotation"):
if tool.Drawing.is_camera_orthographic():
@@ -546,6 +550,156 @@ class CreateDrawing(bpy.types.Operator):
self.file.createIfcDirection(forward.tolist()),
)
def generate_bisect_linework(self, context: bpy.types.Context, root):
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
for obj in context.visible_objects:
if obj.type != "MESH":
continue
if not (element := tool.Ifc.get_entity(obj)):
continue
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
continue
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
context.scene.render.engine = "BLENDER_WORKBENCH"
context.scene.render.use_freestyle = True
context.scene.svg_export.use_svg_export = True
linesets = context.view_layer.freestyle_settings.linesets
if len(linesets) == 1 and linesets[0].name == "LineSet":
context.view_layer.freestyle_settings.crease_angle = radians(140)
context.view_layer.freestyle_settings.use_culling = True
lineset = linesets[0]
lineset.edge_type_negation = "EXCLUSIVE"
lineset.select_silhouette = False
lineset.select_crease = False
lineset.select_border = False
lineset.select_edge_mark = False
lineset.select_contour = False
lineset.select_external_contour = False
lineset.select_material_boundary = False
lineset.select_suggestive_contour = True
lineset.select_ridge_valley = True
edge_mesh = bpy.data.meshes.new("Temp Merged Edges")
edge_obj = bpy.data.objects.new("Temp Merged Edges", edge_mesh)
context.scene.collection.objects.link(edge_obj)
edge_bm = bmesh.new()
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
is_visible = obj.name in visible_object_names
obj.hide_render = not is_visible
if (
is_visible
and obj.type == "MESH"
and len(obj.data.edges)
and not len(obj.data.polygons)
and not obj.name.startswith("IfcAnnotation")
):
tmp_mesh = None
try:
tmp_mesh = obj.data.copy()
tmp_mesh.transform(obj.matrix_world)
edge_bm.from_mesh(tmp_mesh)
finally:
if tmp_mesh:
bpy.data.meshes.remove(tmp_mesh)
ret = bmesh.ops.extrude_edge_only(edge_bm, edges=edge_bm.edges)
verts_extruded = [e for e in ret["geom"] if isinstance(e, bmesh.types.BMVert)]
cam_z = self.camera.matrix_world.to_3x3() @ self.camera.data.view_frame(scene=None)[-1].normalized()
cam_z *= 0.001
for v in verts_extruded:
v.co += cam_z
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
actual_path = svg_path[0:-4] + "0001.svg"
context.scene.render.filepath = svg_path[0:-4]
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
bpy.data.objects.remove(edge_obj)
bpy.data.meshes.remove(edge_mesh)
context.scene.render.use_freestyle = False
context.scene.svg_export.use_svg_export = False
tree = etree.parse(svg_path)
root = tree.getroot()
freestyle_width = float(root.attrib["width"])
freestyle_height = float(root.attrib["height"])
svg_width = self.svg_writer.width
svg_height = self.svg_writer.height
group = root.find(".//{http://www.w3.org/2000/svg}g")
group.attrib["class"] = "projection"
# Resize Freestyle to our proper width / height and purge all other attributes
for path in root.findall(".//{http://www.w3.org/2000/svg}path"):
for key in path.attrib:
if key == "fill":
continue
elif key != "d":
del path.attrib[key]
continue
d = path.attrib[key]
coords = d.strip().split()[1:]
new_d = "M"
for i in range(0, len(coords), 2):
x = float(coords[i][:-1])
y = float(coords[i + 1])
x = x / freestyle_width * svg_width
y = y / freestyle_height * svg_height
new_d += f" {x},{y}"
path.attrib["d"] = new_d
pass
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -632,10 +786,15 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
@@ -720,7 +879,7 @@ class CreateDrawing(bpy.types.Operator):
path.set("class", " ".join(list(classes)))
group.insert(0, path)
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SVGFILL":
results = etree.tostring(root).decode("utf8")
svg_data_1 = results
from xml.dom.minidom import parseString
@@ -885,7 +1044,6 @@ class CreateDrawing(bpy.types.Operator):
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
self.svg_writer.calculate_scale()
x_offset = self.svg_writer.raw_width / 2
y_offset = self.svg_writer.raw_height / 2
@@ -990,6 +1148,7 @@ class CreateDrawing(bpy.types.Operator):
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
for edge in bm.edges:
if not edge.is_manifold:
bm.free()
@@ -1010,6 +1169,11 @@ class CreateDrawing(bpy.types.Operator):
except:
continue
def remove_cut_linework(self, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" not in el.get("class", "").split():
el.getparent().remove(el)
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -1073,10 +1237,34 @@ class CreateDrawing(bpy.types.Operator):
if results:
for path in old_paths:
path.getparent().remove(path)
# polygonize_full will create polygons for everything, including
# interior "holes". As a result we do two passes. The first pass
# records polygon interior rings. The second pass uses this to
# check if the exterior ring matches an interior ring. If it does,
# it's a hole. Skip it!
interior_hashes = set()
for result in results:
for geom in result.geoms:
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if isinstance(geom, shapely.Polygon):
for interior in geom.interiors:
# Sorted because coordinate ordering may differ,
# and frozenset because shapely sometimes emits
# duplicate coordinates.
interior_hashes.add(hash(frozenset(sorted(interior.coords))))
elif isinstance(geom, shapely.LineString):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
for result in results:
for geom in result.geoms:
if isinstance(geom, shapely.Polygon):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if hash(frozenset(sorted(geom.exterior.coords))) in interior_hashes:
# This is a "hole", as its exterior perfectly matches an interior.
continue
d = (
"M"
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
@@ -1088,9 +1276,7 @@ class CreateDrawing(bpy.types.Operator):
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
+ " Z"
)
elif isinstance(geom, shapely.LineString):
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
path.attrib["d"] = d
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
@@ -1629,7 +1815,9 @@ class ActivateDrawing(bpy.types.Operator):
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
+25 -2
View File
@@ -390,8 +390,31 @@ class DocProperties(PropertyGroup):
class BIMCameraProperties(PropertyGroup):
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
linework_mode: EnumProperty(
items=[
("OPENCASCADE", "OpenCASCADE", "Slower, more accurate, with more features"),
("FREESTYLE", "Freestyle", "Faster, less accurate, no fill support"),
],
default="OPENCASCADE",
name="Linework Mode",
)
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
("SHAPELY", "Shapely", "Recommended"),
("SVGFILL", "SVGFill", "Experimental"),
],
default="NONE",
name="Fill Mode",
)
cut_mode: EnumProperty(
items=[
("BISECT", "Bisect", "Faster, more forgiving to bad geometry"),
("OPENCASCADE", "OpenCASCADE", "More technically correct"),
],
default="BISECT",
name="Cut Mode",
)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
+9 -3
View File
@@ -64,9 +64,15 @@ class BIM_PT_camera(Panel):
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = self.layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = self.layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row.prop(props, "linework_mode")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
elif not hasattr(context.scene, "svg_export"):
row = self.layout.row()
row.label(text="Freestyle SVG Exporter Not Installed", icon="ERROR")
row = self.layout.row()
row.prop(props, "width")
+18 -21
View File
@@ -52,41 +52,28 @@ def get_sites(self, context):
def update_latlong(self, context):
sun_props = context.scene.sun_pos_properties
sun_props.latitude = self.latitude
sun_props.longitude = self.longitude
update_sun_path()
update_sun_path(self)
def update_hourminute(self, context):
sun_props = context.scene.sun_pos_properties
sun_props.time = self.hour + (self.minute / 60)
update_sun_path()
update_sun_path(self)
def update_date(self, context):
sun_props = context.scene.sun_pos_properties
sun_props.month = self.month
sun_props.day = self.day
update_sun_path()
update_sun_path(self)
def update_true_north(self, context):
sun_props = context.scene.sun_pos_properties
# Preserve IFC sign convention
sun_props.north_offset = radians(self.true_north * -1)
update_sun_path()
update_sun_path(self)
def update_sun_path_size(self, context):
sun_props = context.scene.sun_pos_properties
sun_props.sun_distance = self.sun_path_size
update_sun_path()
update_sun_path(self)
def update_display_shadows(self, context):
if self.display_shadows:
update_sun_path()
update_sun_path(self)
context.scene.render.engine = "BLENDER_WORKBENCH"
context.scene.display.shading.light = "FLAT"
context.scene.display.shading.show_shadows = True
@@ -102,7 +89,7 @@ def update_display_shadows(self, context):
def update_display_sun_path(self, context):
if self.display_sun_path:
update_sun_path()
update_sun_path(self)
SolarDecorator.install(bpy.context)
else:
SolarDecorator.uninstall()
@@ -113,7 +100,7 @@ def update_resolution(self, context):
context.scene.render.resolution_y = self.radiance_resolution_y
def update_sun_path():
def update_sun_path(self):
if not SolarData.is_loaded:
SolarData.load()
@@ -122,6 +109,16 @@ def update_sun_path():
props = bpy.context.scene.BIMSolarProperties
sun_props = bpy.context.scene.sun_pos_properties
sun_props.sun_distance = self.sun_path_size
sun_props.latitude = self.latitude
sun_props.longitude = self.longitude
sun_props.month = self.month
sun_props.day = self.day
sun_props.time = self.hour + (self.minute / 60)
# Preserve IFC sign convention
sun_props.north_offset = radians(self.true_north * -1)
props.timezone = tzfpy.get_tz(props.longitude, props.latitude)
timezone = pytz.timezone(props.timezone)
dt = datetime.datetime(sun_props.year, sun_props.month, sun_props.day, props.hour, props.minute)
+50 -50
View File
@@ -375,9 +375,8 @@ class PolylineDecorator:
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
last_point_data = polyline_data[len(polyline_data) - 1]
except:
last_point_data = None
second_to_last_point_data = None
default_container_elevation = 0
last_point_data = None
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
@@ -387,9 +386,14 @@ class PolylineDecorator:
last_point = Vector((0, 0, 0))
if is_input_on:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
)
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))
@@ -405,18 +409,22 @@ class PolylineDecorator:
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))
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(snap_vector, last_point, second_to_last_point, degrees=True)
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, 4))
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
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, 4))
cls.input_panel["D"] = str(round(distance, 4))
cls.input_panel["A"] = str(round(angle, 4))
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
@@ -470,13 +478,13 @@ class PolylineDecorator:
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
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:
return
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))
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.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(
@@ -485,35 +493,33 @@ class PolylineDecorator:
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 - 10, last_point.y, last_point.z))
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_rad = radians(180 - float(cls.input_panel["A"]))
ref_vec = second_to_last_point - last_point
dir_vec = last_point - snap_vector
angle = radians(float(cls.input_panel["A"]))
rot_axis = ref_vec.cross(dir_vec)
rot_axis.normalize()
rot_axis = Vector((abs(rot_axis.x), abs(rot_axis.y), abs(rot_axis.z)))
rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
rot_mat = Matrix.Rotation(angle_rad, 3, rot_axis)
ref_vec.normalize()
coords = ((ref_vec @ rot_mat) * distance) + last_point
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, 4))
cls.input_panel["Y"] = str(round(y, 4))
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, 4))
cls.input_panel["Z"] = str(round(z, 3))
return cls.input_panel
cls.input_panel["X"] = str(round(last_point.x, 3))
cls.input_panel["Y"] = str(round(last_point.y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(last_point.z, 3))
return cls.input_panel
def draw_batch(self, shader_type, content_pos, color, indices=None):
@@ -522,9 +528,8 @@ class PolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_input_panel(self, context):
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
@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
@@ -532,6 +537,15 @@ class PolylineDecorator:
else:
precision = None
factor = 1
if context.scene.unit_settings.length_unit == "MILLIMETERS":
factor = 1000
return format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
def draw_input_panel(self, context):
texts = {"D": "Distance: ", "A": "Angle: ", "X": "X coord: ", "Y": "Y coord: ", "Z": "Z coord:", "AREA": "Area: "}
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
@@ -546,11 +560,7 @@ class PolylineDecorator:
if key != "A" and key != self.input_type:
value = float(value)
if context.scene.unit_settings.length_unit == "MILLIMETERS":
value = value * 1000
formatted_value = format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
formatted_value = self.format_input_panel_units(context, value)
else:
formatted_value = value
@@ -582,25 +592,15 @@ 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)
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
value = measurement_prop[i].dim
value = float(value)
if context.scene.unit_settings.length_unit == "MILLIMETERS":
value = value * 1000
formatted_value = format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
formatted_value = self.format_input_panel_units(context, value)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
if i == 1:
continue
pos_angle = measurement_prop[i - 1].position
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
+5 -5
View File
@@ -40,7 +40,8 @@ import json
import collections
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
def update_door_modifier_representation(context: bpy.types.Context) -> None:
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -534,10 +535,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_door"
bl_label = "Add Door"
bl_options = {"REGISTER"}
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
@@ -558,7 +558,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
)
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
return {"FINISHED"}
@@ -612,7 +612,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
+17 -2
View File
@@ -429,7 +429,15 @@ class DrawPolylineWall(bpy.types.Operator):
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.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()
@@ -437,7 +445,13 @@ class DrawPolylineWall(bpy.types.Operator):
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
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()
@@ -507,6 +521,7 @@ class DrawPolylineWall(bpy.types.Operator):
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)
+5 -5
View File
@@ -36,7 +36,8 @@ from bmesh.types import BMVert
from mathutils import Vector
def update_window_modifier_representation(context, obj):
def update_window_modifier_representation(context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
ifc_file = tool.Ifc.get()
@@ -427,10 +428,9 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_window"
bl_label = "Add Window"
bl_options = {"REGISTER"}
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
@@ -450,7 +450,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
)
update_window_modifier_representation(context, obj)
update_window_modifier_representation(context)
return {"FINISHED"}
@@ -502,7 +502,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_window_modifier_representation(context, obj)
update_window_modifier_representation(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
@@ -1712,7 +1712,7 @@ class LoadLinkedProject(bpy.types.Operator):
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
if material_ids.size > 0:
if material_ids.size > 0 and len(mesh.polygons) == len(material_ids):
mesh.polygons.foreach_set("material_index", material_ids)
mesh.update()
@@ -2327,7 +2327,7 @@ class MeasureTool(bpy.types.Operator):
self.number_is_negative = False
self.is_input_on = False
self.input_options = ["D", "A", "X", "Y", "Z"]
self.input_type = "OFF"
self.input_type = None
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None
@@ -2353,9 +2353,9 @@ class MeasureTool(bpy.types.Operator):
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[self.input_type] = self.number_output
# 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
@@ -2365,7 +2365,7 @@ class MeasureTool(bpy.types.Operator):
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
self.mousemove_count += 1
self.is_input_on = False
self.input_type = "OFF"
self.input_type = None
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
@@ -2382,7 +2382,6 @@ class MeasureTool(bpy.types.Operator):
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)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return {"RUNNING_MODAL"}
@@ -2416,7 +2415,15 @@ class MeasureTool(bpy.types.Operator):
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.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()
@@ -2424,7 +2431,13 @@ class MeasureTool(bpy.types.Operator):
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
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()
@@ -2436,11 +2449,12 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type in {"D", "A"} and not event.shift:
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()
@@ -2464,12 +2478,18 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
PolylineDecorator.uninstall()
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.input_type = None
self.number_input = []
self.number_output = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -2507,8 +2527,9 @@ class MeasureTool(bpy.types.Operator):
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"
self.input_type = None
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
@@ -78,14 +78,18 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Container"
bl_description = "Assign current default container to the selected objects"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
def _execute(self, context):
props = context.active_object.BIMObjectSpatialProperties
if (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
for element_obj in context.selected_objects:
core.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
)
if self.container:
container = tool.Ifc.get().by_id(self.container)
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
pass
for element_obj in context.selected_objects:
core.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
)
class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator):
+4 -2
View File
@@ -159,16 +159,18 @@ class BIM_PT_spatial_decomposition(Panel):
if not self.props.total_elements:
row = self.layout.row(align=True)
row.label(text=f"{self.props.active_container.ifc_class} > No Contained Elements", icon="FILE_3D")
row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D")
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
return
row = self.layout.row(align=True)
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Contained Elements",
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
op.container = ifc_definition_id
+10 -10
View File
@@ -414,8 +414,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_window(obj=obj.name)
with context.temp_override(active_object=obj):
bpy.ops.bim.add_window()
elif template == "DOOR":
mesh = bpy.data.meshes.new(name)
@@ -429,8 +429,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_door(obj=obj.name)
with context.temp_override(active_object=obj):
bpy.ops.bim.add_door()
elif template == "STAIR":
mesh = bpy.data.meshes.new(name)
@@ -444,8 +444,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class=ifc_class,
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_stair()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_stair()
elif template == "RAILING":
mesh = bpy.data.meshes.new(name)
@@ -460,8 +460,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_railing()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_railing()
elif template == "ROOF":
mesh = bpy.data.meshes.new(name)
@@ -476,8 +476,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_roof()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_roof()
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
props.type_class = props.type_class
+6 -1
View File
@@ -41,8 +41,8 @@ def disable_editing_cost_schedule(cost: tool.Cost) -> None:
def remove_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
cost.remove_stored_schedule_columns(cost_schedule)
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
@@ -398,3 +398,8 @@ def add_currency(ifc: tool.Ifc, cost: tool.Cost) -> ifcopenshell.entity_instance
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
ifc.run("unit.assign_unit", units=[unit])
return unit
def generate_cost_schedule_browser(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> bpy.types.Panel:
cost_schedule_data = cost.create_cost_schedule_json(cost_schedule)
return cost.generate_cost_schedule_browser(cost_schedule_data)
+22 -17
View File
@@ -85,34 +85,39 @@ class Cad:
return math.degrees(a) if degrees else a
@classmethod
def angle_3_vectors(cls, v1, v2, v3, degrees=False):
def angle_3_vectors(cls, v1, v2, v3, new_angle=None, degrees=False):
"""
> takes 3 vectors. The order matters, v2 is the center point.
< returns the potentially signed angle as degrees or radians
< returns the signed angle as degrees or radians
< if a new angle is provided, return the rotation vector
"""
d1 = v1 - v2
d2 = v2 - v3
d2 = v3 - v2
axis = d1.cross(d2)
axis.normalize()
axis = Vector((abs(axis.x), abs(axis.y), abs(axis.z)))
d1.normalize()
d2.normalize()
rotation_axis = d1.cross(d2)
axis = d1.cross(d2).normalized()
# Calculate the unsigned angle between the "from" and "to" vectors
# Calculate the unsigned angle between the "d1" and "d2" vectors
a = d1.angle(d2)
# Determine the sign of the angle based on the provided axis
if degrees:
a = math.degrees(a)
parameter = rotation_axis.dot(axis)
sign = 1 if parameter <= 0 else -1
return a * sign
# If new_angle, determine the direction of the rotation
parameter = round(axis.z, 2) < 0 or (round(axis.y, 2) == 0 and round(axis.x < 0)) or (round(axis.x, 2) == 0 and round(axis.y < 0))
if new_angle:
rot_mat = Matrix.Rotation(new_angle, 3, axis)
rot_vector = (d1 @ rot_mat) if parameter else (rot_mat @ d1)
return rot_vector
else:
return a
sign = -1 if parameter else 1
if degrees:
a = math.degrees(a)
return a * sign
else:
return a
@classmethod
def is_x(cls, value: float, x: float, tolerance: float | None = None) -> bool:
+8 -3
View File
@@ -811,9 +811,8 @@ class Cost(bonsai.core.tool.Cost):
@classmethod
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
from bonsai.bim.module.cost.data import CostSchedulesData
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
data = []
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects or []:
@@ -852,3 +851,9 @@ class Cost(bonsai.core.tool.Cost):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def generate_cost_schedule_browser(cls, cost_schedule_data: list[dict[str, Any]]) -> None:
if not bpy.context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="costing")
tool.Web.send_webui_data(data=cost_schedule_data, data_key="cost_items", event="cost_items")
+7 -4
View File
@@ -524,14 +524,14 @@ class Snap(bonsai.core.tool.Snap):
def validate_input(cls, input_number, input_type):
grammar_imperial = """
start: FORMULA? dim expr?
start: (FORMULA dim expr) | dim
dim: imperial
FORMULA: "="
imperial: feet? "-"? inches?
feet: NUMBER? " "? fraction? "'"
inches: NUMBER? " "? fraction? "\\""
feet: NUMBER? "-"? fraction? "'"
inches: NUMBER? "-"? fraction? "\\""
fraction: NUMBER "/" NUMBER
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
@@ -583,7 +583,10 @@ class Snap(bonsai.core.tool.Snap):
def imperial(self, args):
if len(args) > 1:
result = args[0] + args[1]
if args[0] <= 0:
result = args[0] - args[1]
else:
result = args[0] + args[1]
else:
result = args[0]
return result
+65 -7
View File
@@ -21,6 +21,7 @@ from bonsai.bim.module.web.data import WebData
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.api.sequence
import ifcopenshell.api.cost
from typing import Any, Dict, Optional
import time
import socket
@@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "loadCostSchedule":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
if operator_data["type"] == "addCostItem":
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"]))
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "selectAssignedElements":
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"] == "addCostItem":
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"]))
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(
@@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web):
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)
cls.send_webui_data(data={
"cost_values": cost_values,
"cost_item_id": operator_data["costItemId"]
}, data_key="cost_values", event="cost_values")
if operator_data["type"] == "addCostValue":
value = ifcopenshell.api.cost.add_cost_value(
ifc_file,
parent=ifc_file.by_id(operator_data["costItemId"]),
)
cls.send_webui_data(
data={
"cost_value_id" : value.id(),
"cost_item_id":operator_data["costItemId"]},
data_key="cost_value",
event="cost_value"
)
if operator_data["type"] == "editCostValues":
cost_item_id = operator_data["costItemId"]
print('Editing cost values', cost_item_id)
print(cost_item_id)
print(type(operator_data["costValues"]))
for value_data in operator_data["costValues"] or []:
print(value_data)
value = ifc_file.by_id(value_data["id"])
print(value.get_info())
if value_data["costType"] == "FIXED":
attributes= {
"AppliedValue": value_data["appliedValue"],
}
elif value_data["costType"] == "CATEGORY":
attributes= {
"AppliedValue": value_data["appliedValue"],
"Category": value_data["costCategory"],
}
elif value_data["costType"] == "SUM":
attributes= {
"Category": '*'
}
ifcopenshell.api.cost.edit_cost_value(
file=ifc_file,
cost_value=value,
attributes= attributes
)
tool.Cost.load_cost_schedule_tree()
cost_item = ifc_file.by_id(operator_data["costItemId"])
if not cost_item:
print("Cost item not found")
return
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
@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=json_data, data_key="cost_items", event="cost_items")
@classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None:
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -9,6 +9,7 @@ This chapter covers how you can help contribute to Bonsai.
:hidden:
:maxdepth: 2
installation
hello_world
running_tests
translations
@@ -3,20 +3,16 @@ Installation
There are different methods of installation, depending on your situation.
1. **Unstable installation** is recommended for power users helping with testing.
2. **Bundling for Blender** is recommended for distributing the add-on.
3. **Live development environment** is recommended for developers who are actively coding.
4. **Packaged installation** is recommended for those who use a package manager.
1. :ref:`guides/development/installation:Unstable installation` is recommended
for power users helping with testing.
2. :ref:`guides/development/installation:Bundling for blender` is recommended for distributing the add-on.
3. :ref:`guides/development/installation:Live development environment` is
recommended for developers who are actively coding.
4. :ref:`guides/development/installation:Packaged installation` is recommended
for those who use a package manager.
Unstable installation
---------------------
**Unstable installation** is almost the same as **Stable installation**, except
that they are typically updated every day. Simply download a daily build from
the `GitHub releases page
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
then follow the usual :doc:`installation
instructions</users/quickstart/installation>`.
System requirements
-------------------
Bonsai officially supports all major 64-bit platforms, as well as the Python
version shipped by the Blender Foundation for the most recent three major
@@ -41,6 +37,76 @@ Other system specifications match the `Blender Requirements
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
but it happens.
Unstable installation
---------------------
**Unstable installation** is almost the same as **Stable installation**, except
that they are typically updated every day. To install the **Unstable** version:
1. Open up Blender, and click on ``Edit > Preferences``.
.. image:: /quickstart/images/install-bonsai-1.png
2. Select the **Get Extensions** tab, and press **Allow Online Access**.
.. image:: /quickstart/images/install-bonsai-2.png
3. Go to the `Bonsai Unstable Repository
<https://github.com/IfcOpenShell/bonsai_unstable_repo>`__, and drag and drop
from the appropriate link in the ``ID`` column of the table into Blender
depending on your operating system.
.. image:: images/unstable-drag-drop.png
4. Enable **Check for Updates on Startup** to get updates for daily Bonsai
builds automatically.
.. image:: images/unstable-auto-update.png
.. tip::
Instead of drag and drop, you can manually create the repository:
Open :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
--> Repositories (Top Right) --> "+" Icon --> Add Remote Remository`.
You'll see a window similar to the one above.
Use as URL:
``https://raw.githubusercontent.com/IfcOpenShell/bonsai_unstable_repo/main/index.json``
and enable **Check for Updates on Startup** if you want them.
5. Search for **Bonsai** in the top left search bar, then press the **Install**
button.
.. image:: /quickstart/images/install-bonsai-3.png
.. warning::
Make sure the extension you install has ``raw.githubusercontent.com`` as
it's "Repository" (not ``extensions.blender.org``).
.. image:: images/unstable-repo.png
6. Whenever a new update is available, you'll see it in the bottom right
:menuselection:`Status Bar`
.. image:: images/unstable-icon.png
7. To update, click on the update button in :menuselection:`Topbar --> Edit -->
Preferences --> Get Extensions`.
.. image:: /guides/images/update.png
8. After an update, be sure to restart.
.. image:: images/unstable-restart.png
If you wish to install an **Unstable** version offline, you can download a
daily build from the `GitHub releases page
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
then go to :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
--> "V" Icon (top right) --> Install from Disk`.
Bundling for Blender
--------------------
@@ -72,12 +138,13 @@ Live development environment
----------------------------
One option for developers who want to actively develop from source is to follow
the instructions from :ref:`devs/installation:Bundling for Blender`. However,
the instructions from :ref:`guides/development/installation:Bundling for Blender`. However,
creating a build, uninstalling the old add-on, and installing a new build is a
slow process. Although it works, it is very slow, so we do not recommend it.
A more rapid approach is to follow the :ref:`devs/installation:Unstable
installation` method, as this provides all dependencies for you out of the box.
A more rapid approach is to follow the
:ref:`guides/development/installation:Unstable installation` method, as this
provides all dependencies for you out of the box.
Once you've done this, you can replace certain Python files that tend to be
updated frequently with those from the Git repository. We're going to use
+6 -5
View File
@@ -30,13 +30,13 @@ Updating
We always recommend to use the latest version.
Open up Blender, click on ``Edit > Preferences``, and select the **Get
Extensions** tab. If an update is available, you will see a button next to the
Open up Blender, click on :menuselection:`Topbar --> Edit --> Preferences -->
Get Extensions`. If an update is available, you will see a button next to the
**Bonsai** add-on.
Updates are typically available every 2 months. If you need something more
frequent, check out :ref:`devs/installation:unstable installation` which is
updated every day.
frequent, check out :ref:`guides/development/installation:Unstable
installation` which is updated every day.
.. image:: images/update.png
@@ -94,7 +94,8 @@ Blender geometry that represents what the model might've looked at at some
point. At worst, you might be looking at a completely wrong model.
If you continue to open and save ``.blend`` files, you will run the risk of
editing something that doesn't actually exist in your IFC model. This will
editing something that doesn't actually exist in your IFC model (e.g you can
meet an error similar to "RuntimeError: Instance #1234 not found"). This will
create unpredictable, and sometimes unrecoverable errors.
To avoid this issue, only open and save IFCs.
+1 -2
View File
@@ -5,7 +5,6 @@ Scenario: Viewing default solar settings
Given an empty IFC project
And I look at the "Solar Access / Shadow" panel
Then I see "Etc/GMT"
And I see "Sunrise: 06:02:50"
Scenario: Changing the month
Given an empty IFC project
@@ -33,7 +32,7 @@ Scenario: Automatic timezone detection based on lat / long
When I set the "Latitude" property to "10.0"
And I set the "Longitude" property to "20.0"
Then I see "Africa/Ndjamena"
And I see "Sunrise: 05:29:43"
And I see "Sunrise: 05:56:42"
Scenario: Display the sun path
Given an empty IFC project
+1 -1
View File
@@ -27,7 +27,7 @@ Scenario: Resize to storey
And the object "IfcWall/Cube" is selected
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
And I press "bim.set_default_container(container={storey})"
And I press "bim.assign_container()"
And I press "bim.assign_container(container={storey})"
When I press "bim.resize_to_storey(total_storeys=1)"
Then nothing happens
+2 -2
View File
@@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64)
PLATFORMTAG:=win_amd64
endif
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-7e6607a-$(PLATFORM).zip
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-7e6607a-$(PLATFORM).zip
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
.PHONY: test
test:
@@ -246,6 +246,26 @@ def get_cost_item_assignments(
]
def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]:
results = []
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(calculate_applied_value(cost_item, cost_value))
label += " = {}".format(serialise_cost_value(cost_value))
results.append(
{
"id": cost_value.id(),
"label": label,
"name": cost_value.Name,
"category": cost_value.Category,
"applied_value": (
get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None
),
}
)
print(results)
return results
class CostValueUnserialiser:
def parse(self, formula: str):
l = lark.Lark(
+4
View File
@@ -52,6 +52,10 @@ class IFC_PARSE_API HeaderEntity {
return data_.size();
}
AttributeValue getArgument(size_t index) const {
return data_.get_attribute_value(index);
}
std::string toString(bool upper = false) const {
std::stringstream stream;
stream << datatype_;
@@ -136,21 +136,21 @@ class Patcher:
if material.is_a("IfcMaterial"):
materials = []
elif material.is_a("IfcMaterialLayerSet"):
for idx, item in enumerate(material.MaterialLayers):
for idx, item in enumerate(material.MaterialLayers or []):
material = item.Material
properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)])
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
if category := getattr(material, "Category", None):
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
elif material.is_a("IfcMaterialProfileSet"):
for idx, item in enumerate(material.MaterialProfiles):
for idx, item in enumerate(material.MaterialProfiles or []):
material = item.Material
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
if category := getattr(material, "Category", None):
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
elif material.is_a("IfcMaterialConstituentSet"):
for idx, item in enumerate(material.MaterialConstituents):
for idx, item in enumerate(material.MaterialConstituents or []):
material = item.Material
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name])
+20
View File
@@ -520,6 +520,26 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas
}
}
// Expose FileDescription and FileName header entities
// to make them readable even if they were not filled properly before.
// Though it is invalid IFC, technically.
// FileSchema is not exposed as IFC file won't load if it's invalid.
%extend IfcParse::FileDescription {
AttributeValue description() const { return $self->getArgument(0); }
AttributeValue implementation_level() const { return $self->getArgument(1); }
};
%extend IfcParse::FileName {
AttributeValue name() const { return $self->getArgument(0); }
AttributeValue time_stamp() const { return $self->getArgument(1); }
AttributeValue author() const { return $self->getArgument(2); }
AttributeValue organization() const { return $self->getArgument(3); }
AttributeValue preprocessor_version() const { return $self->getArgument(4); }
AttributeValue originating_system() const { return $self->getArgument(5); }
AttributeValue authorization() const { return $self->getArgument(6); }
};
%extend IfcParse::IfcSpfHeader {
%pythoncode %{
# Hide the getters with read-only property implementations