mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into light/radiance
This commit is contained in:
+1
-1
@@ -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 = "×";
|
||||
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,
|
||||
|
||||
@@ -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"}
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user