Merge branch 'v0.8.0' into webui_updates

This commit is contained in:
Dion Moult
2024-08-29 09:55:41 +10:00
committed by GitHub
234 changed files with 121241 additions and 190358 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=d51fa2c
OLD:=7e6607a
.PHONY: bump
bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -121,6 +121,20 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["demo_data"] = data
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web")
async def on_work_schedule_info(self, sid, data):
print(f"Work schedule info from Blender client {sid}")
blender_messages[sid]["work_schedule_info"] = data
await sio.emit("work_schedule_info", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_items(self, sid, data):
print(f"Cost items data from Blender client {sid}")
blender_messages[sid]["cost_items"] = data
await sio.emit("cost_items", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_schedules(self, sid, data):
print(f"Cost schedule info from Blender client {sid}")
blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
@@ -128,6 +142,12 @@ async def schedules(request):
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def costing(request):
with open("templates/costing.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def sequencing(request):
with open("templates/gantt.html", "r") as f:
@@ -178,6 +198,7 @@ sio.register_namespace(BlenderNamespace("/blender"))
app.router.add_get("/", schedules)
app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing)
app.router.add_get("/costing", costing)
app.router.add_get("/demo", demo)
# Add static files
@@ -0,0 +1,58 @@
:root.blender .flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
}
/* CSS for the work schedule cards */
:root.blender #work_schedules {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 20px;
}
:root.blender .card {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 300px;
margin: 10px;
transition: transform 0.2s;
}
:root.blender .card:hover {
transform: scale(1.05);
}
:root.blender .card-body {
padding: 20px;
}
:root.blender .card-title {
font-size: 1.25rem;
margin-bottom: 10px;
}
:root.blender .card-text {
font-size: 1rem;
margin-bottom: 20px;
}
:root.blender .btn-primary {
background-color: #007bff;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1rem;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
:root.blender .btn-primary:hover {
background-color: #0056b3;
}
@@ -1,3 +1,5 @@
@import url("./components/card.css");
:root {
--font-family: Arial, sans-serif;
--base-font-size: 16px;
@@ -0,0 +1,168 @@
import { CostUI } from './utilities/costui.js';
const connectedClients = {};
let socket;
$(document).ready(function () {
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
connectSocket();
CostUI.createColorPicker();
});
function connectSocket() {
const url = "ws://localhost:" + SOCKET_PORT + "/web";
socket = io(url);
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connected_clients", handleConnectedClients);
socket.on("theme_data", handleThemeData);
socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData);
}
function handleBlenderConnect(blenderId) {
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = { shown: false, ifc_file: "" };
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
}
function handleBlenderDisconnect(blenderId) {
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeTableElement(blenderId);
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) - 1;
});
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
data.forEach(function (id) {
connectedClients[id] = { shown: false, ifc_file: "" };
});
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
if (a !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
function generateCssVariableRule(theme) {
let cssVariables = ":root.blender {\n";
for (const key in theme) {
const cssVariableName = `--blender-${key.replace(/_/g, "-")}`;
const cssVariableValue = arrayToRgbString(theme[key]);
cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`;
}
cssVariables += "}";
return cssVariables;
}
const cssRule = generateCssVariableRule(themeData.theme);
var styleElement = $("#index-stylesheet")[0];
if (styleElement) {
var sheet = styleElement.sheet || styleElement.styleSheet;
sheet.insertRule(cssRule, sheet.cssRules.length);
}
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
$("#toggle-theme").html('<i class="fas fa-moon"></i>');
} else if (theme === "blender") {
$("#toggle-theme").html('<i class="fas fa-adjust"></i>');
}
localStorage.setItem("theme", theme);
}
function addCostItem(costItemId) {
console.log("addCostItem", costItemId);
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
function editCostItemName(costItemId, name) {
executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name });
}
function selectAssignedElements(costItemId) {
executeOperator({ type: "selectAssignedElements", costItemId: costItemId });
}
function handleWebConnect() {
getCostSchedules();
}
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);
const callback = () => loadCostSchedule(costSchedule.id, blenderId);
const card = CostUI.createCard(costSchedule.Name, mainContainer, callback);
costScheduleDiv.append(card);
});
}
function handleCostItemsData(data) {
console.log(data);
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
callbacks: {
"addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
}
});
}
function executeOperator(operator, blenderId) {
const msg = {
sourcePage: "cost",
operator: operator,
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
}
function loadCostSchedule(costScheduleId, blenderId) {
executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId);
}
function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId);
}
@@ -1,3 +1,5 @@
import { CostUI } from './utilities/costui.js';
// keeps track of blenders connected in form of
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
const connectedClients = {};
@@ -36,9 +38,16 @@ function connectSocket() {
socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connected_clients", handleConnectedClients);
socket.on("connect", handleWebConnect);
socket.on("theme_data", handleThemeData);
socket.on("gantt_data", handleGanttData);
socket.on("default_data", handleDefaultData);
socket.on("work_schedule_info", handleWorkScheduleData);
}
// function used to get drawings data from Bonsai
function handleWebConnect() {
getWorkScheduleData();
}
// Function to handle 'blender_connect' event
@@ -62,6 +71,7 @@ function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeWorkScheduleInfo(blenderId);
removeGanttElement(blenderId);
}
@@ -114,8 +124,25 @@ function handleThemeData(themeData) {
}
}
// Function to handle 'gantt_data' event
function handleWorkScheduleData(data) {
const blenderId = data["blenderId"];
const workSchedulesMainDiv = $("#work_schedules");
const workSchedulesDiv = $("<div>").attr("id", "work_schedules-" + blenderId);
workSchedulesMainDiv.append(workSchedulesDiv);
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);
workSchedulesDiv.append(card);
});
}
function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"];
console.log(data);
@@ -156,7 +183,7 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log(data);
console.log('default data',data);
}
// Function to add a new gantt with data and filename
@@ -551,3 +578,33 @@ function toggleClientList() {
clientList.addClass("show");
}
function loadWorkSchedule(workScheduleId) {
const msg = {
sourcePage: "gantt",
operator: {
type: "loadWorkSchedule",
workScheduleId: workScheduleId,
},
};
socket.emit("web_operator", msg);
}
function removeWorkScheduleInfo(blenderId) {
$("#work_schedules-"+ blenderId).empty();
}
function getWorkScheduleData(blenderId) {
const msg = {
sourcePage: "gantt",
operator: {
type: "getWorkSchedules",
},
};
if (blenderId !== undefined) {
msg.BlenderId = blenderId;
}
socket.emit("web_operator", msg);
}
@@ -0,0 +1,464 @@
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;
}
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
table.id = 'cost-items-' + id;
const tbody = document.createElement("tbody");
tbody.setAttribute("id", "cost-items");
const columnHeaders = ["Name", "Quantity", "Unit", "Cost", "Total Cost", "Action"];
const tr = document.createElement("tr");
for (let i = 0; i < columnHeaders.length; i++) {
const th = document.createElement("th");
th.textContent = columnHeaders[i];
th.style.position = "relative"; // Required for the resizer handle
tr.appendChild(th);
// Add resizer handle
if (i < columnHeaders.length - 1) { // No resizer for the last column
const resizer = document.createElement("div");
resizer.classList.add("resizer");
th.appendChild(resizer);
CostUI.addResizer(resizer);
}
}
tbody.appendChild(tr);
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
// Add CSS to set column widths, resizer styles, hover effect, and color scheme
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu();
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
};
return [table, tbody];
}
static addTableStyles(id) {
const style = document.createElement("style");
style.textContent = `
#cost-items-${id} th:nth-child(1),
#cost-items-${id} td:nth-child(1) {
width: auto;
}
#cost-items-${id} th:not(:nth-child(1)),
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
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() {
// 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="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
`;
document.body.appendChild(contextMenu);
// Add event listeners for context menu
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
const targetRow = event.target.closest("tr");
if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
contextMenu.style.left = `${event.pageX}px`;
contextMenu.style.top = `${event.pageY}px`;
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
} else {
document.getElementById("context-menu").style.display = "none";
}
});
document.addEventListener("click", function(event) {
const contextMenu = document.getElementById("context-menu");
if (!contextMenu.contains(event.target)) {
contextMenu.style.display = "none";
}
});
document.getElementById("edit-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your edit action here
console.log("Edit row:", targetRow.getAttribute("id"));
}
});
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your delete action here
console.log("Delete row:", targetRow.getAttribute("id"));
targetRow.remove();
}
});
document.getElementById("duplicate-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
// Implement your duplicate action here
console.log("Duplicate row:", targetRow.getAttribute("id"));
const newRow = targetRow.cloneNode(true);
targetRow.parentElement.appendChild(newRow);
}
});
}
static addResizer(resizer) {
let startX, startWidth, th;
resizer.addEventListener("mousedown", function(e) {
th = e.target.parentElement;
startX = e.pageX;
startWidth = th.offsetWidth;
document.addEventListener("mousemove", resizeColumn);
document.addEventListener("mouseup", stopResize);
});
function resizeColumn(e) {
const newWidth = startWidth + (e.pageX - startX);
th.style.width = newWidth + "px";
}
function stopResize() {
document.removeEventListener("mousemove", resizeColumn);
document.removeEventListener("mouseup", stopResize);
}
}
static generateColorScheme(baseColor) {
// This function generates a color scheme based on the base color
// For simplicity, we'll just lighten the base color for each level
const levels = 7; // Number of levels of nesting
const colorScheme = [];
for (let i = 0; i < levels; i++) {
colorScheme.push(CostUI.lightenColor(baseColor, i * 7));
}
return colorScheme;
}
static lightenColor(color, percent) {
// This function lightens a color by a given percentage
const num = parseInt(color.slice(1), 16),
amt = Math.round(2.55 * percent),
R = (num >> 16) + amt,
G = (num >> 8 & 0x00FF) + amt,
B = (num & 0x0000FF) + amt;
return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`;
}
static applyColorScheme(tableId, colorScheme) {
const rows = document.querySelectorAll(`#${tableId} tbody tr`);
rows.forEach((row, index) => {
const level = index % colorScheme.length; // Assuming level is determined by index for simplicity
row.style.backgroundColor = colorScheme[level];
});
}
static createColorPicker() {
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.id = "color-picker";
colorPicker.value = "#ff0000"; // Default color
const colorText = document.createElement("p");
colorText.textContent = "Select a color to change row color:";
document.getElementById("UI").appendChild(colorText);
document.getElementById("UI").appendChild(colorPicker);
colorPicker.addEventListener("input", function() {
const baseColor = colorPicker.value;
const colorScheme = CostUI.generateColorScheme(baseColor);
CostUI.applyColorScheme("cost-items", colorScheme);
});
const colorScheme = CostUI.generateColorScheme("#000000");
CostUI.applyColorScheme("cost-items", colorScheme);
}
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
const [table, tbody] = CostUI.createTable(blenderID);
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) {
data.forEach(obj => {
const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks);
container.appendChild(row);
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks);
}
});
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID);
if (nestingLevel > 0) {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
expandButton.textContent = ">";
} else {
expandButton.style.visibility = "hidden";
}
//row.appendChild(expandButton);
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
});
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);
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
row.appendChild(totalCostCell);
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
});
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;
}
static hideNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.add("nested");
const childId = row.getAttribute('id');
if (childId) {
CostUI.hideNestedRows(childId);
}
});
}
static showNestedRows(parentId) {
const rows = document.querySelectorAll(`[parent-id='${parentId}']`);
rows.forEach(row => {
row.classList.remove("nested");
const childId = row.getAttribute('id');
if (childId && CostUI.isRowExpanded(childId)) {
CostUI.showNestedRows(childId);
}
});
}
static contractExpandRow(id) {
const rows = document.querySelectorAll(`[parent-id='${id}']`);
if (rows.length === 0) {
return;
}
let isVisible = false;
rows.forEach(row => {
if (!row.classList.contains("nested")) {
isVisible = true;
}
});
if (isVisible) {
CostUI.hideNestedRows(id);
this.textContent = ">";
CostUI.updateExpandedState(id, false);
} else {
CostUI.showNestedRows(id);
this.textContent = "^";
CostUI.updateExpandedState(id, true);
}
}
static updateExpandedState(id, isExpanded) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
expandedState[id] = isExpanded;
localStorage.setItem('expandedState', JSON.stringify(expandedState));
}
static applyExpandedState() {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
Object.keys(expandedState).forEach(id => {
if (expandedState[id]) {
CostUI.showNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = "^";
}
} else {
CostUI.hideNestedRows(id);
const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`);
if (toggleButton) {
toggleButton.textContent = ">";
}
}
});
}
static isRowExpanded(id) {
const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {};
return expandedState[id] || false;
}
static text(label) {
const text = document.createElement("p");
text.textContent = label;
return text;
}
static createCard(title, mainContainer, callback) {
const card = document.createElement("div");
card.classList.add("card");
const cardBody = document.createElement("div");
cardBody.classList.add("card-body");
const cardTitle = document.createElement("h5");
cardTitle.classList.add("card-title");
cardTitle.textContent = title;
const cardButton = document.createElement("button");
cardButton.classList.add("btn", "btn-primary");
cardButton.textContent = "Load";
cardButton.addEventListener("click", callback);
cardBody.appendChild(cardTitle);
cardBody.appendChild(mainContainer);
cardBody.appendChild(cardButton);
card.appendChild(cardBody);
return card;
}
}
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en" class="blender">
<head>
<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"
id="tabulator-stylesheet"
href="https://unpkg.com/tabulator-tables/dist/css/tabulator_site_dark.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
/>
<script
type="text/javascript"
src="https://code.jquery.com/jquery-3.6.0.min.js"
></script>
<script
type="text/javascript"
src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"
></script>
<script
type="text/javascript"
src="https://cdn.socket.io/4.0.0/socket.io.min.js"
></script>
<script>
var SOCKET_PORT = {{port}};
</script>
<script type="module" defer src="./static/js/cost.js"></script>
<style>
table {
width: 100%;
border-collapse: collapse;
}
.nested {
display: none;
}
.expand-collapse {
cursor: pointer;
}
.caret-cell {
border: none;
background: none;
padding: 0;
}
td {
justify-content: space-between;
}
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
</head>
<body>
<nav>
<img
src="https://bonsaibim.org/assets/images/blender/blender-logo.png"
alt="Logo"
class="logo"
/>
<ul>
<li>
<a href="/"
><i class="fa-solid fa-table"></i> Schedules</a
>
</li>
<li>
<a href="/costing" class="active"
><i class="fa-solid fa-table"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
<li>
<a><i class="fa-solid fa-camera"></i> BCF Topics</a>
</li>
<li>
<a><i class="fa-solid fa-square-check"></i> IDS Audits</a>
</li>
<li>
<a><i class="fa-solid fa-hotel"></i> Facility Management</a>
</li>
</ul>
<button id="toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
</div>
<div id="cost-schedules"></div>
<div id="UI"></div>
<div id="cost-items"></div>
<footer>
<p>BlenderBIM Version: {{version}}</p>
</footer>
</body>
</html>
@@ -58,6 +58,11 @@
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -27,7 +27,7 @@
<script>
var SOCKET_PORT = {{port}};
</script>
<script defer src="./static/js/gantt.js"></script>
<script type="module" defer src="./static/js/gantt.js"></script>
</head>
<body>
<nav class="no-print">
@@ -40,6 +40,11 @@
<li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing" class="active"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -76,6 +81,7 @@
</button>
<div id="client-list" class="no-print"></div>
</div>
<div id="work_schedules"></div>
<div id="container"></div>
<footer>
<p>Bonsai Version: {{version}}</p>
@@ -49,6 +49,11 @@
><i class="fa-solid fa-table"></i> Schedules</a
>
</li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li>
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
+27 -8
View File
@@ -117,17 +117,34 @@ class IfcStore:
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
cache_path = Path(IfcStore.cache_path)
cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
cache_preexists = cache_path.exists()
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
if os.path.exists(IfcStore.cache_path):
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
return
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
if cache_preexists:
print(f"Successfully loaded existing cache: {cache_path.name}.")
else:
print("New cache was created.")
except Exception as e:
if cache_preexists:
print(f"Failed to create a cache from existing file '{cache_path.name}': {str(e)}.")
else:
print(f"Failed to create a cache: {str(e)}.")
# No point to trying again the same operation.
return
os.remove(IfcStore.cache_path)
try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
IfcStore.cache_path, cache_settings, serializer_settings
)
print("New cache was created.")
except Exception as e:
print(f"Failed to create a cache: {str(e)}.")
return
return IfcStore.cache
@@ -135,6 +152,8 @@ class IfcStore:
def update_cache():
if not IfcStore.cache:
return
assert IfcStore.cache_path
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest()
new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5")
@@ -440,9 +440,11 @@ class SelectExpressFile(bpy.types.Operator):
class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache"
bl_label = "Purge HDF5 Cache"
bl_description = "Clean up HDF5 cache files except the ones that currently loaded"
def execute(self, context):
core.purge_hdf5_cache(tool.Debug)
self.report({"INFO"}, "HDF5 cache purged.")
return {"FINISHED"}
@@ -31,6 +31,7 @@ classes = (
operator.LoadParentDocument,
operator.LoadProjectDocuments,
operator.RemoveDocument,
operator.SelectDocumentObjects,
operator.UnassignDocument,
prop.Document,
prop.BIMDocumentProperties,
@@ -20,6 +20,7 @@ import bpy
import json
import ifcopenshell.api
import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.tool as tool
import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore
@@ -149,3 +150,25 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj)
if element:
core.unassign_document(tool.Ifc, product=element, document=document)
class SelectDocumentObjects(bpy.types.Operator):
bl_idname = "bim.select_document_objects"
bl_label = "Select Document Objects"
bl_options = {"REGISTER", "UNDO"}
document: bpy.props.IntProperty(name="Document ID", default=0)
def execute(self, context):
if not self.document or not (relating_document := tool.Ifc.get_entity_by_id(self.document)):
self.report({"INFO"}, f"No document found by id '{self.document}'.")
return {"FINISHED"}
i = 0
for element in ifcopenshell.util.element.get_referenced_elements(relating_document):
obj = tool.Ifc.get_object(element)
if not obj or obj not in context.selectable_objects:
continue
obj.select_set(True)
i += 1
self.report({"INFO"}, f"{i} objects selected.")
return {"FINISHED"}
@@ -66,6 +66,9 @@ class BIM_PT_documents(Panel):
row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif self.props.documents and self.props.active_document_index < len(self.props.documents):
ifc_definition_id = self.props.documents[self.props.active_document_index].ifc_definition_id
row.operator("bim.select_document_objects", text="", icon="RESTRICT_SELECT_OFF").document = (
ifc_definition_id
)
row.operator("bim.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").document = ifc_definition_id
@@ -202,7 +202,7 @@ def format_distance(
if not isArea:
add_inches = bool(inches) or not suppress_zero_inches
tx_dist = ""
if feet:
if feet is not None:
tx_dist += str(feet) + "'"
if feet and add_inches:
tx_dist += " - "
@@ -1463,7 +1463,7 @@ class ActivateModel(bpy.types.Operator):
bl_idname = "bim.activate_model"
bl_label = "Activate Model"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the model view"
bl_description = "Activate the model view, hide all annotations"
def execute(self, context):
dprops = bpy.context.scene.DocProperties
@@ -1471,10 +1471,23 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall()
# save current visibility statuses
# Preserve current visibility statuses for:
# - non-ifc objects
# - type product
# - annotations (so we won't unhide other drawings)
ifc_file = tool.Ifc.get()
visibility_status: dict[bpy.types.Object, bool] = {}
for obj in bpy.data.objects:
visibility_status[obj] = obj.hide_get()
element = tool.Ifc.get_entity(obj)
if not element:
hide = obj.hide_get()
elif element.is_a("IfcAnnotation"):
hide = True
elif element.is_a("IfcTypeProduct"):
hide = obj.hide_get()
else:
continue
visibility_status[obj] = hide
if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()):
@@ -1779,7 +1792,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.index:
index = int(self.index)
else:
index = context.active_object.data.BIMCameraProperties.active_drawing_style_index
index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index
scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data()
@@ -1875,7 +1888,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
def set_raster_style(self, context):
def set_raster_style(self, context: bpy.types.Context) -> None:
scene = context.scene # Do not remove. It is used in exec later
space = self.get_view_3d(context) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style)
@@ -1889,7 +1902,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
# Differences in Blender versions mean result in failures here
print(f"Failed to set shading style {path} to {value}")
def set_query(self, context):
def set_query(self, context: bpy.types.Context) -> None:
self.include_global_ids = []
self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files:
@@ -1908,7 +1921,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.drawing_style.exclude_query:
self.parse_filter_query("EXCLUDE", context)
def parse_filter_query(self, mode, context):
def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None:
if mode == "INCLUDE":
objects = context.scene.objects
elif mode == "EXCLUDE":
@@ -1927,7 +1940,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if global_id in self.exclude_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H
def get_view_3d(self, context):
def get_view_3d(self, context: bpy.types.Context) -> bpy.types.Space:
for area in context.screen.areas:
if area.type != "VIEW_3D":
continue
@@ -1935,6 +1948,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if space.type != "VIEW_3D":
continue
return space
assert False, "Space is not found."
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -2678,8 +2692,9 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
filter_mode: bpy.props.StringProperty()
def _execute(self, context):
props = context.active_object.data.BIMCameraProperties
obj = bpy.context.scene.camera
assert obj
props = obj.data.BIMCameraProperties
element = tool.Ifc.get_entity(obj)
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
if self.filter_mode == "INCLUDE":
+7 -2
View File
@@ -259,8 +259,12 @@ def update_should_draw_decorations(self, context):
continue
tool.Drawing.update_text_value(obj)
refresh_drawing_data()
if bpy.app.background:
return
decoration.DecorationsHandler.install(context)
else:
if bpy.app.background:
return
decoration.DecorationsHandler.uninstall()
@@ -531,9 +535,10 @@ def get_relating_type_id(self, context):
def update_annotation_object_type(self, context):
self.relating_type_id = "0"
# changing enum doesn't trigger refresh by itself
# Refresh enum items before changing property,
# otherwise it might map to the wrong item.
AnnotationData.is_loaded = False
self.relating_type_id = "0"
def update_sheet_data(self, context):
+5 -11
View File
@@ -106,11 +106,7 @@ class BIM_PT_element_filters(Panel):
@classmethod
def poll(cls, context):
return (
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
if not ElementFiltersData.is_loaded:
@@ -160,17 +156,15 @@ class BIM_PT_drawing_underlay(Panel):
@classmethod
def poll(cls, context):
return (
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
def draw(self, context):
layout = self.layout
layout.use_property_split = True
camera = context.scene.camera
assert camera
dprops = context.scene.DocProperties
props = context.active_object.data.BIMCameraProperties
props = camera.data.BIMCameraProperties
drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
if not DrawingsData.is_loaded:
@@ -184,6 +184,8 @@ def create_annotation():
create_annotation_occurrence(bpy.context)
else:
object_type = props.object_type
if not bpy.ops.bim.add_annotation.poll():
return
bpy.ops.bim.add_annotation(
object_type=object_type, data_type=tool.Drawing.ANNOTATION_TYPES_DATA[object_type][-1]
)
@@ -216,6 +216,7 @@ def register():
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
workspace.load_custom_icons()
def unregister():
if not bpy.app.background:
@@ -241,3 +242,4 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
workspace.unload_custom_icons()
+106 -33
View File
@@ -28,6 +28,7 @@ from bpy_extras import view3d_utils
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
def transparent_color(color, alpha=0.1):
@@ -306,6 +307,8 @@ class PolylineDecorator:
angle_snap_mat = None
angle_snap_loc = None
use_default_container = False
instructions = None
snap_info = None
@classmethod
def install(cls, context):
@@ -313,6 +316,9 @@ class PolylineDecorator:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (context,), "WINDOW", "POST_PIXEL")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@@ -353,6 +359,14 @@ class PolylineDecorator:
cls.plane_origin = plane_origin
cls.plane_normal = plane_normal
@classmethod
def set_instructions(cls, instructions):
cls.instructions = instructions
@classmethod
def set_snap_info(cls, snap_info):
cls.snap_info = snap_info
@classmethod
def calculate_distance_and_angle(cls, context, is_input_on):
@@ -391,7 +405,7 @@ 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 = (snap_vector - last_point).length
if distance > 0:
@@ -451,7 +465,6 @@ class PolylineDecorator:
cls.input_panel["AREA"] = str(round(area, 4))
return cls.input_panel
@classmethod
def calculate_x_y_and_z(cls, context):
try:
@@ -472,7 +485,7 @@ 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 - 10, last_point.y, last_point.z))
distance = float(cls.input_panel["D"])
@@ -510,7 +523,16 @@ class PolylineDecorator:
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:"}
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
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
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
blf.size(self.font_id, 12)
@@ -521,6 +543,17 @@ class PolylineDecorator:
offset = 20
new_line = 20
for i, (key, value) in enumerate(self.input_panel.items()):
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
)
else:
formatted_value = value
if key not in list(texts.keys()):
continue
if key == self.input_type:
@@ -528,36 +561,72 @@ class PolylineDecorator:
else:
blf.color(self.font_id, *color)
blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0)
blf.draw(self.font_id, texts[key] + value)
blf.draw(self.font_id, texts[key] + formatted_value)
def draw_measurements(self, context):
region = context.region
rv3d = region.data
measurement_prop = context.scene.BIMModelProperties.polyline_measurement
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
self.font_id = 1
blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
for i in range(len(measurement_prop)):
if i == 0:
continue
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i-1].position)) / 2
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + measurement_prop[i].dim)
pos_angle = measurement_prop[i-1].position
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
)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
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)
blf.draw(self.font_id, "a: " + measurement_prop[i].angle)
def draw_on_screen_menu(self, context):
region = context.region
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 2
blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
text_w, text_h = blf.dimensions(0, self.instructions)
position = (region.width / 2) - (text_w / 2)
blf.position(self.font_id, position, 10, 0)
blf.draw(self.font_id, self.instructions)
text_w, text_h = blf.dimensions(0, self.snap_info)
position = (region.width / 2) - (text_w / 2)
blf.position(self.font_id, position, 30, 0)
blf.draw(self.font_id, self.snap_info)
def __call__(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -588,14 +657,6 @@ class PolylineDecorator:
except:
ref_point = None
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
if ref_point:
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = []
if self.use_default_container:
@@ -608,7 +669,7 @@ class PolylineDecorator:
edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges)
# Polyline with selected points
# Create polyline with selected points
polyline_data = context.scene.BIMModelProperties.polyline_point
polyline_points = []
polyline_edges = []
@@ -619,18 +680,6 @@ class PolylineDecorator:
for i in range(len(polyline_points) - 1):
polyline_edges.append([i, i + 1])
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
# Line for angle axis snap
if snap_prop.snap_type == "Axis":
@@ -649,3 +698,27 @@ class PolylineDecorator:
for i in range(1, len(polyline_points) - 1):
edges.append((0, i, i + 1))
self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
# Mouse points
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
if ref_point:
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
# Draw polyline with selected points
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
+72 -21
View File
@@ -294,7 +294,27 @@ class DrawPolylineWall(bpy.types.Operator):
self.action_count = 0
self.visible_objs = []
self.objs_2d_bbox = []
self.number_options = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-", "*", "/"}
self.number_options = {
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" ",
".",
"+",
"-",
"*",
"/",
"'",
'"',
"=",
}
self.number_input = []
self.number_output = ""
self.number_is_negative = False
@@ -304,23 +324,32 @@ class DrawPolylineWall(bpy.types.Operator):
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": ""}
self.snap_angle = None
self.snapping_points = []
self.instructions = """TAB: Cycle Input
M: Modify Snap Point
C: Close
Backspace: Remove
X Y: Axis
Shift: Lock axis
"""
def recalculate_inputs(self, context):
if self.number_input:
is_valid, self.number_output = tool.Snap.validate_input(self.number_output)
is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
self.input_panel[self.input_type] = self.number_output
if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else:
if self.input_type in {"X", "Y"}:
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
else:
self.input_panel[self.input_type] = self.number_output
tool.Blender.update_viewport()
return is_valid
# TODO This is creating a hack in generate function from DumbWallGenerator
# Come up with a better solution
@@ -356,7 +385,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref()
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
@@ -364,10 +393,11 @@ class DrawPolylineWall(bpy.types.Operator):
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
@@ -381,6 +411,14 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "X":
tool.Snap.set_snap_axis_method("X")
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "Y":
tool.Snap.set_snap_axis_method("Y")
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline()
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -411,11 +449,12 @@ class DrawPolylineWall(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type in self.input_options:
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()
@@ -447,8 +486,9 @@ class DrawPolylineWall(bpy.types.Operator):
return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
self.recalculate_inputs(context)
tool.Snap.insert_polyline_point(self.input_panel)
is_valid = self.recalculate_inputs(context)
if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False
self.input_type = "OFF"
self.number_input = []
@@ -456,6 +496,12 @@ class DrawPolylineWall(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
@@ -467,6 +513,7 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Blender.update_viewport()
else:
if event.value == "RELEASE" and event.type in {"ESC"}:
tool.Snap.set_snap_axis_method(None)
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
@@ -480,11 +527,13 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Snap.set_use_default_container(True)
PolylineDecorator.set_use_default_container(True)
tool.Snap.set_snap_plane_method("XY")
PolylineDecorator.set_instructions(self.instructions)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
@@ -669,7 +718,7 @@ class DumbWallGenerator:
for stroke in layer.active_frame.strokes:
if len(stroke.points) == 1:
continue
data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co))
data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co), round=True)
if data:
strokes.append(data)
objs.append(data["obj"])
@@ -694,19 +743,21 @@ class DumbWallGenerator:
bpy.context.scene.grease_pencil.layers.remove(layer)
return objs
def create_wall_from_2_points(self, coords):
def create_wall_from_2_points(self, coords, round=False):
direction = coords[1] - coords[0]
length = direction.length
if length < 0.1:
return
data = {"coords": coords}
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
self.length = length
self.rotation = math.atan2(direction[1], direction[0])
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
if round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_wall()
return data
+400 -99
View File
@@ -17,7 +17,9 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import bpy
import bpy.utils.previews
import bonsai.tool as tool
import bonsai.core.model as core
from bonsai.bim.module.model.wall import DumbWallJoiner
@@ -27,11 +29,29 @@ from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.system.data import PortData
from bonsai.bim.module.model.prop import get_ifc_class
custom_icon_previews = None
def load_custom_icons():
global custom_icon_previews
icons_dir = os.path.join(os.path.dirname(__file__), "..", "..", "data", "icons")
custom_icon_previews = bpy.utils.previews.new()
for entry in os.scandir(icons_dir):
if entry.name.endswith(".png"):
name = os.path.splitext(entry.name)[0]
custom_icon_previews.load(name.upper(), entry.path, "IMAGE")
def unload_custom_icons():
global custom_icon_previews
if custom_icon_previews:
bpy.utils.previews.remove(custom_icon_previews)
custom_icon_previews = None
class BimTool(WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "bim.bim_tool"
bl_label = "BIM Tool"
bl_description = "Create and edit elements by construction class"
@@ -220,19 +240,36 @@ class CableTool(BimTool):
BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey_operator(layout, text, hotkey, description):
modifiers = {
"A": "EVENT_ALT",
"C": "EVENT_CTRL",
"S": "EVENT_SHIFT",
}
MODIFIERS = {
"A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"),
"C": ("EVENT_CTRL", "CTRL"),
"S": ("EVENT_SHIFT", ""),
}
def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""):
modifier, key = hotkey.split("_")
row = layout.row(align=True)
row.label(text="", icon=modifiers[modifier])
row.label(text="", icon=f"EVENT_{key}")
try:
custom_icon = custom_icon_previews[text.upper().replace(" ", "_")].icon_id
except KeyError:
custom_icon = custom_icon_previews["IFC"].icon_id
modifier_icon, modifier_str = MODIFIERS[modifier]
if ui_context == "TOOL_HEADER":
op = layout.operator("bim.hotkey", text="", icon_value=custom_icon)
else:
row = layout.row(align=True)
op = row.operator("bim.hotkey", text=text, icon_value=custom_icon)
row.label(text="", icon=modifier_icon)
row.label(text="", icon=f"EVENT_{key}")
hotkey_description = f"Hotkey: {modifier_str} {key}"
if description:
description += "\n\n"
description += hotkey_description
op = row.operator("bim.hotkey", text=text)
op.hotkey = hotkey
op.description = description
return op
@@ -260,14 +297,19 @@ class BimToolUI:
AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER":
cls.draw_header_interface()
elif context.region.type in ("UI", "WINDOW"):
# same interface for both n-panel sidebar and object properties
cls.draw_basic_bim_tool_interface()
cls.draw_container(context)
cls.draw_type_selection_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_header_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_interface(context)
elif not context.selected_objects:
elif context.region.type in ("UI", "WINDOW"):
cls.draw_container(context)
cls.draw_thumbnail()
cls.draw_type_selection_interface(context)
if context.active_object and context.selected_objects:
cls.draw_edit_object_panel_interface(context)
if not context.selected_objects:
cls.draw_create_object_interface()
@classmethod
@@ -285,10 +327,10 @@ class BimToolUI:
row.prop(data=cls.props, property="length", text="Length")
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
elif cls.props.ifc_class in ("IfcSlabType", "IfcRampType", "IfcRoofType"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
elif cls.props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis")
@@ -318,47 +360,73 @@ class BimToolUI:
row.prop(data=cls.props, property="rl_mode", text="RL")
@classmethod
def draw_edit_object_interface(cls, context):
def draw_edit_object_panel_interface(cls, context):
ui_context = str(context.region.type)
if AuthoringData.data["active_material_usage"] == "LAYER2":
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="extrusion_depth", text="Height")
row.prop(data=cls.props, property="extrusion_depth", text="Height:")
op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="length", text="Length")
row.prop(data=cls.props, property="length", text="Length:")
op = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = cls.props.length
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle:")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__, ui_context)
cls.layout.separator()
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"Extends/reduces element to 3D cursor",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"Intersects two non-parallel elements to a butt corner junction",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"Join two intersecting walls using a mitre joint.\nOther selected wall is connected to the active",
"Intersects two non-parallel elements to a mitred corner junction",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__)
row.operator("bim.unjoin_walls", icon="X", text="")
row = cls.layout.row(align=True)
row.operator(
"bim.unjoin_walls", text="Unjoin Walls", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id
)
cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, ui_context)
# row.operator("bim.unjoin_walls", icon="X", text="")
elif AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1:
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif "LAYER2" in AuthoringData.data["selected_material_usages"]:
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context)
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="X Angle")
row.prop(data=cls.props, property="x_angle", text="Angle")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
@@ -376,8 +444,14 @@ class BimToolUI:
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__)
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
@@ -386,18 +460,37 @@ class BimToolUI:
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "")
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "", ui_context)
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "")
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "")
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__)
add_layout_hotkey_operator(
cls.layout,
"Edit Axis",
"A_E",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__, ui_context
)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
@@ -422,15 +515,15 @@ class BimToolUI:
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "", ui_context)
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "")
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif AuthoringData.data["active_class"] in ("IfcSpace",):
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__)
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__, ui_context)
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
row = cls.layout.row(align=True)
@@ -439,25 +532,25 @@ class BimToolUI:
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__, ui_context
)
cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) > 1:
row.operator("bim.add_opening", text="Apply Void")
row.operator("bim.add_opening", text="Apply Void", icon_value=custom_icon_previews["VOID"].icon_id)
else:
row.operator("bim.add_potential_opening", text="Add Void")
row.operator(
"bim.add_potential_opening", text="Add Void", icon_value=custom_icon_previews["ADD_VOID"].icon_id
)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row = cls.layout.row(align=True)
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
else:
row.operator("bim.show_openings", icon="HIDE_OFF", text="")
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
@@ -469,51 +562,264 @@ class BimToolUI:
row.operator("bim.clone_opening", text="Clone Opening")
cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "")
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "")
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "")
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
add_layout_hotkey_operator(cls.layout, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Centerline", "S_C", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Interior", "S_V", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings", ui_context)
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition", ui_context)
cls.layout.row(align=True).label(text="Aggregation")
add_layout_hotkey_operator(cls.layout, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__)
add_layout_hotkey_operator(cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__)
cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__
cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__, ui_context
)
cls.layout.row(align=True).label(text="Qto")
add_layout_hotkey_operator(
cls.layout, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__, ui_context
)
@classmethod
def draw_header_interface(cls):
cls.draw_type_selection_interface()
if AuthoringData.data["ifc_classes"]:
def draw_edit_object_header_interface(cls, context):
ui_context = str(context.region.type)
if AuthoringData.data["active_material_usage"] == "LAYER2":
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
row.prop(data=cls.props, property="extrusion_depth", text="Height:")
op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="length", text="Length:")
op = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = cls.props.length
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="Angle:")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
row = cls.layout.row()
add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__, ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(
row,
"Extend",
"S_E",
"Extends/reduces element to 3D cursor",
ui_context,
)
add_layout_hotkey_operator(
row,
"Butt",
"S_T",
"Intersects two non-parallel elements to a butt corner junction",
ui_context,
)
add_layout_hotkey_operator(
row,
"Mitre",
"S_Y",
"Intersects two non-parallel elements to a mitred corner junction",
ui_context,
)
row.operator("bim.unjoin_walls", text="", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__, ui_context)
add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, ui_context)
add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, ui_context)
elif AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1:
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif "LAYER2" in AuthoringData.data["selected_material_usages"]:
add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context)
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="x_angle", text="Angle")
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle
elif AuthoringData.data["active_material_usage"] == "PROFILE":
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis")
op = row.operator("bim.change_cardinal_point", icon="FILE_REFRESH", text="")
op.cardinal_point = int(cls.props.cardinal_point)
row = cls.layout.row(align=True)
label = (
"Height" if AuthoringData.data["active_class"] in ("IfcColumn", "IfcColumnStandardCase") else "Length"
)
row.prop(data=cls.props, property="extrusion_depth", text=label)
op = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(
cls.layout,
"Extend",
"S_E",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__, ui_context)
if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment",
"IfcCableSegment",
"IfcDuctSegment",
"IfcPipeSegment",
):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "", ui_context)
if context.region.type != "TOOL_HEADER":
cls.layout.operator("bim.mep_add_bend")
cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_obstruction")
else:
add_layout_hotkey_operator(
cls.layout,
"Edit Axis",
"A_E",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Butt",
"S_T",
"",
ui_context,
)
add_layout_hotkey_operator(
cls.layout,
"Mitre",
"S_Y",
"",
ui_context,
)
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context)
add_layout_hotkey_operator(
cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__, ui_context
)
row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif (
tool.Model.is_parametric_railing_active() and not context.active_object.BIMRailingProperties.is_editing_path
):
# NOTE: should be above "active_representation_type" = "SweptSolid" check
# because it could be a SweptSolid too
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_TAB")
row.operator("bim.enable_editing_railing_path", text="Edit Railing Path")
elif AuthoringData.data["active_class"] in (
"IfcWindow",
"IfcWindowStandardCase",
"IfcDoor",
"IfcDoorStandardCase",
):
if AuthoringData.data["active_class"] in ("IfcWindow", "IfcWindowStandardCase"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl2", text="RL")
elif AuthoringData.data["active_class"] in ("IfcDoor", "IfcDoorStandardCase"):
row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL")
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "", ui_context)
elif AuthoringData.data["active_representation_type"] == "SweptSolid":
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active():
add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "", ui_context)
elif AuthoringData.data["active_class"] in ("IfcSpace",):
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.generate_space.__doc__, ui_context)
elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_TAB")
row.operator("bim.enable_editing_roof_path", text="Edit Roof Path")
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator(
cls.layout, "Regen MEP", "S_G", bpy.ops.bim.regenerate_distribution_element.__doc__, ui_context
)
cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True)
if len(context.selected_objects) > 1:
row.operator("bim.add_opening", text="", icon_value=custom_icon_previews["VOID"].icon_id)
else:
row.operator("bim.add_potential_opening", text="", icon_value=custom_icon_previews["ADD_VOID"].icon_id)
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row = cls.layout.row(align=True)
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", text="")
if len(context.selected_objects) == 2:
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_L")
row.operator("bim.clone_opening", text="Clone Opening")
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(row, "Centerline", "S_C", "", ui_context)
add_layout_hotkey_operator(row, "Interior", "S_V", "", ui_context)
add_layout_hotkey_operator(row, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__, ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Void", "A_O", "Toggle openings", ui_context)
add_layout_hotkey_operator(row, "Decomposition", "A_D", "Select decomposition", ui_context)
row = cls.layout.row(align=True)
add_layout_hotkey_operator(row, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__, ui_context)
add_layout_hotkey_operator(row, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__, ui_context)
row = cls.layout.row()
add_layout_hotkey_operator(
row, "Perform Quantity Take-off", "S_Q", bpy.ops.bim.perform_quantity_take_off.__doc__, ui_context
)
@classmethod
def draw_type_selection_interface(cls):
def draw_container(cls, context):
text = AuthoringData.data["default_container"]
if context.region.type == "UI":
text = f"Container: {text}"
cls.layout.row(align=True).label(text=text, icon="OUTLINER_COLLECTION")
@classmethod
def draw_type_selection_interface(cls, context):
# shared by both sidebar and header
ui_context = str(context.region.type)
row = cls.layout.row(align=True)
row.label(text=f"Container: {AuthoringData.data['default_container']}", icon="OUTLINER_COLLECTION")
if AuthoringData.data["ifc_classes"]:
if ui_context == "UI":
text = "Add"
else:
text = ""
if not AuthoringData.data["ifc_element_type"]:
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "ifc_class", text="")
row.operator("bim.add_constr_type_instance", text=text, icon_value=custom_icon_previews["ADD"].icon_id)
row = cls.layout.row(align=True)
if AuthoringData.data["relating_type_id"]:
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="")
row.operator("bim.add_constr_type_instance", text=text, icon_value=custom_icon_previews["ADD"].icon_id)
else:
row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
@@ -524,7 +830,9 @@ class BimToolUI:
row.prop(cls.props, "type_name")
row = cls.layout.row(align=True)
op = row.operator(
"bim.add_default_type", icon="ADD", text=f"Add {AuthoringData.data['ifc_element_type']}"
"bim.add_default_type",
icon_value=custom_icon_previews["ADD_TYPE"].icon_id,
text=f"Create {AuthoringData.data['ifc_element_type']}",
)
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="")
@@ -533,9 +841,7 @@ class BimToolUI:
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod
def draw_basic_bim_tool_interface(cls):
cls.draw_type_selection_interface()
def draw_thumbnail(cls):
if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class:
box = cls.layout.box()
@@ -545,18 +851,10 @@ class BimToolUI:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_A")
op = row.operator("bim.add_constr_type_instance", text="Add")
op.from_invoke = True
if cls.props.relating_type_id.isnumeric():
op.relating_type_id = int(cls.props.relating_type_id)
class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hotkey"
bl_label = "Hotkey"
bl_label = ""
bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -843,7 +1141,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.z = self.z
def hotkey_S_P(self):
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
mode = bpy.context.mode
current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode)
if current_tool.idname == "bim.wall_tool":
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
def hotkey_S_L(self):
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
@@ -2303,7 +2303,27 @@ class MeasureTool(bpy.types.Operator):
self.action_count = 0
self.visible_objs = []
self.objs_2d_bbox = []
self.number_options = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-", "*", "-", "/"}
self.number_options = {
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
" ",
".",
"+",
"-",
"*",
"/",
"'",
'"',
"=",
}
self.number_input = []
self.number_output = ""
self.number_is_negative = False
@@ -2313,23 +2333,33 @@ class MeasureTool(bpy.types.Operator):
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None
self.snapping_points = []
self.instructions = """TAB: Cycle Input
M: Modify Snap Point
C: Close
Backspace: Remove
X Y Z: Axis
S-(X Y Z): Plane
Shift: Lock axis
"""
def recalculate_inputs(self, context):
if self.number_input:
is_valid, self.number_output = tool.Snap.validate_input(self.number_output)
is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
self.input_panel[self.input_type] = self.number_output
if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else:
if self.input_type in {"X", "Y", "Z"}:
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return is_valid
def modal(self, context, event):
@@ -2339,7 +2369,7 @@ class MeasureTool(bpy.types.Operator):
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref()
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
@@ -2347,10 +2377,11 @@ class MeasureTool(bpy.types.Operator):
if self.mousemove_count == 2:
self.objs_2d_bbox = []
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -2365,6 +2396,18 @@ class MeasureTool(bpy.types.Operator):
tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "X":
tool.Snap.set_snap_axis_method("X")
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "Y":
tool.Snap.set_snap_axis_method("Y")
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "Z":
tool.Snap.set_snap_axis_method("Z")
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline()
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -2395,7 +2438,7 @@ 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 self.input_options and not event.shift:
if event.value == "PRESS" and event.type in {"D", "A"} and not event.shift:
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = event.type
@@ -2424,8 +2467,9 @@ class MeasureTool(bpy.types.Operator):
tool.Blender.update_viewport()
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
self.recalculate_inputs(context)
tool.Snap.insert_polyline_point(self.input_panel)
is_valid = self.recalculate_inputs(context)
if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False
self.input_type = "OFF"
self.number_input = []
@@ -2433,20 +2477,32 @@ 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 == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
if event.shift and event.value == "PRESS" and event.type == "X":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("YZ")
tool.Snap.cycle_snap_plane_method("YZ")
tool.Snap.set_snap_axis_method(None)
tool.Blender.update_viewport()
if event.shift and event.value == "PRESS" and event.type == "Y":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("XZ")
tool.Snap.cycle_snap_plane_method("XZ")
tool.Snap.set_snap_axis_method(None)
tool.Blender.update_viewport()
if event.shift and event.value == "PRESS" and event.type == "Z":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("XY")
tool.Snap.cycle_snap_plane_method("XY")
tool.Snap.set_snap_axis_method(None)
tool.Blender.update_viewport()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
@@ -2459,6 +2515,8 @@ class MeasureTool(bpy.types.Operator):
tool.Blender.update_viewport()
else:
if event.value == "RELEASE" and event.type in {"ESC"}:
tool.Snap.set_snap_plane_method(None)
tool.Snap.set_snap_axis_method(None)
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
@@ -2471,12 +2529,15 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.install(context)
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
tool.Snap.set_snap_plane_method("No Plane")
tool.Snap.set_snap_plane_method(None)
tool.Snap.set_snap_axis_method(None)
PolylineDecorator.set_instructions(self.instructions)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
+4 -3
View File
@@ -84,12 +84,13 @@ class ColourByPropertyData:
@classmethod
def colourscheme_key(cls):
default = [("QUERY", "Custom Query", "Specify a custom query to colour by"), None]
obj = bpy.context.active_object
if not obj:
return []
return default
element = tool.Ifc.get_entity(obj)
if not element:
return []
return default
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element)
for pset, properties in psets.items():
@@ -100,7 +101,7 @@ class ColourByPropertyData:
else:
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys]
return [("QUERY", "Custom Query", "Specify a custom query to colour by"), None] + results
return default + results
class SelectSimilarData:
@@ -124,6 +124,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_description = "Edit the underlying filter query for advanced users"
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query")
old_query: StringProperty(name="Old Query")
module: StringProperty()
def _execute(self, context):
@@ -38,6 +38,7 @@ classes = (
operator.SelectSimilarContainer,
operator.SetContainerVisibility,
operator.SetDefaultContainer,
operator.ToggleContainerElement,
prop.Element,
prop.BIMObjectSpatialProperties,
prop.BIMContainer,
@@ -243,6 +243,17 @@ class DeleteContainer(bpy.types.Operator, tool.Ifc.Operator):
core.delete_container(tool.Ifc, tool.Spatial, tool.Geometry, container=tool.Ifc.get().by_id(self.container))
class ToggleContainerElement(bpy.types.Operator):
bl_idname = "bim.toggle_container_element"
bl_label = "Toggle Container Element"
bl_options = {"REGISTER", "UNDO"}
element_index: bpy.props.IntProperty()
def execute(self, context):
core.toggle_container_element(tool.Spatial, element_index=self.element_index)
return {"FINISHED"}
class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_decomposed_elements"
bl_label = "Select Children"
@@ -260,25 +271,33 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context)
def _execute(self, context):
ifc_class = relating_type = is_untyped = None
ifc_class = relating_type = None
is_untyped = False
ifc_file = tool.Ifc.get()
if self.should_filter:
active_element = context.scene.BIMSpatialDecompositionProperties.active_element
if active_element.is_class:
element_type = active_element.type
if element_type == "CLASS":
ifc_class = active_element.name
elif relating_type := active_element.ifc_definition_id:
elif element_type == "TYPE":
ifc_class = active_element.ifc_class
relating_type = tool.Ifc.get().by_id(relating_type)
else:
ifc_class = active_element.ifc_class
is_untyped = True
if ifc_id := active_element.ifc_definition_id:
relating_type = ifc_file.by_id(ifc_id)
else: # OCCURRENCE
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
obj = tool.Ifc.get_object(occurrence)
assert isinstance(obj, bpy.types.Object)
tool.Blender.set_active_object(obj)
return
element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter
core.select_decomposed_elements(
tool.Spatial,
container=tool.Ifc.get().by_id(self.container),
container=ifc_file.by_id(self.container),
ifc_class=ifc_class,
relating_type=relating_type,
is_untyped=is_untyped,
element_filter = element_filter
element_filter=element_filter,
)
+15 -5
View File
@@ -107,21 +107,31 @@ class BIMContainer(PropertyGroup):
class Element(PropertyGroup):
name: StringProperty(name="Name")
ifc_class: StringProperty(name="Name")
is_class: BoolProperty(name="Is Class", default=False)
is_type: BoolProperty(name="Is Type", default=False)
ifc_definition_id: IntProperty(name="IFC Definition ID")
ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'")
ifc_definition_id: IntProperty(
name="IFC Definition ID",
description="ID of the element type / occurrence. 0 if 'type' is 'CLASS' or if it's 'TYPE' but it represents untyped elements",
)
total: IntProperty(name="Total")
is_expanded: BoolProperty(name="Is Expanded", default=False)
type: EnumProperty(
name="Element Type",
items=(
("CLASS", "CLASS", "CLASS"),
("TYPE", "TYPE", "TYPE"),
("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"),
),
)
class BIMSpatialDecompositionProperties(PropertyGroup):
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]")
expanded_containers: StringProperty(name="Expanded containers", default="[]")
active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"})
elements: CollectionProperty(name="Elements", type=Element)
expanded_elements: StringProperty(name="Expanded Elements", default="{}")
active_element_index: IntProperty(name="Active Element Index")
total_elements: IntProperty(name="Total Elements")
subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class")
+14 -4
View File
@@ -258,21 +258,31 @@ class BIM_UL_elements(UIList):
def __init__(self):
self.use_filter_show = True
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int):
icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT"
row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag):
if item:
row = layout.row(align=True)
if item.is_class:
row.label(text="", icon="DISCLOSURE_TRI_DOWN")
item_type = item.type
if item_type == "CLASS":
self.draw_toggle(row, item.is_expanded, index)
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
elif item.is_type:
elif item_type == "TYPE":
row.label(text="", icon="BLANK1")
self.draw_toggle(row, item.is_expanded, index)
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
else: # OCCURRENCE
for _ in range(2):
row.label(text="", icon="BLANK1")
row.label(text=item.name)
def draw_filter(self, context, layout):
row = layout.row()
+12 -3
View File
@@ -17,15 +17,24 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
def parse_express(debug, filename):
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
import bpy
import ifcopenshell
import bonsai.tool as tool
def parse_express(debug: tool.Debug, filename: str) -> None:
debug.add_schema_identifier(debug.load_express(filename))
def purge_hdf5_cache(debug):
def purge_hdf5_cache(debug: tool.Debug) -> None:
debug.purge_hdf5_cache()
def purge_unused_elements(ifc, debug, ifc_class):
def purge_unused_elements(ifc, debug: tool.Debug, ifc_class: str) -> int:
ifc_file = ifc.get()
unused_elements = [i for i in ifc_file.by_type(ifc_class) if ifc_file.get_total_inverses(i) == 0]
unused_elements_amount = len(unused_elements)
+1
View File
@@ -122,6 +122,7 @@ def switch_representation(
return
entity = ifc.get_entity(obj)
assert entity
current_obj_data = geometry.get_object_data(obj)
if not current_obj_data and geometry.is_text_literal(representation):
+1 -1
View File
@@ -86,7 +86,7 @@ def assign_class(
obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None
)
if default_container := root.get_default_container():
if not root.is_drawing_annotation(element) and (default_container := root.get_default_container()):
if root.is_spatial_element(element):
ifc.run("aggregate.assign_object", products=[element], relating_object=default_container)
elif root.is_containable(element):
+5
View File
@@ -141,6 +141,11 @@ def delete_container(
spatial.import_spatial_decomposition()
def toggle_container_element(spatial: tool.Spatial, element_index: int) -> None:
spatial.toggle_container_element(element_index)
spatial.load_contained_elements()
def select_decomposed_elements(
spatial: tool.Spatial,
container: ifcopenshell.entity_instance,
+1
View File
@@ -723,6 +723,7 @@ class Root:
def get_object_representation(cls, obj): pass
def get_representation_context(cls, representation): pass
def is_containable(cls, element): pass
def is_drawing_annotation(cls, element): pass
def is_element_a(cls, element, ifc_class): pass
def is_spatial_element(cls, element): pass
def link_object_data(cls, source_obj, destination_obj): pass
+46
View File
@@ -142,6 +142,14 @@ class Cad:
def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float | None = None) -> bool:
return cls.is_x((v2 - v1).length, 0, tolerance)
@classmethod
def intersect_edge_plane(cls, v1, v2, plane_co, plane_no):
"""
> takes an edges as two vector, and a plane as origin point and normal
< return the intersection point or None
"""
return geometry.intersect_line_plane(v1, v2, plane_co, plane_no)
@classmethod
def intersect_edges(cls, edge1, edge2):
"""
@@ -163,6 +171,44 @@ class Cad:
return r1.to_2d() if r1 else r1, r2.to_2d() if r2 else r2
return results
@classmethod
def intersect_edges_v2(cls, edge1, edge2):
"""
Calculate the closest points on two line segments.
Note: This function doesn't use intersect_line_line
> edge1: tuple of two vectors (v1, v2) representing the first segment
> edge2: tuple of two vectors (v3, v4) representing the second segment
< returns: tuple of two vectors (C1, C2) or (None, None) if lines are parallel
"""
# This function seems to work better then intersect_line_line
# in orthogonal view
# https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
# Starting and ending points
P1, P1_end = edge1
P2, P2_end = edge2
# Directions
d1 = (P1_end - P1).normalized()
d2 = (P2_end - P2).normalized()
n = d1.cross(d2)
# if n is zero, lines are parallel
if n.length == 0:
return None, None
n2 = d2.cross(n)
C1 = P1 + ((P2 - P1).dot(n2) / (d1.dot(n2))) * d1
n1 = d1.cross(n)
C2 = P2 + ((P1 - P2).dot(n1) / (d2.dot(n1))) * d2
return C1, C2
@classmethod
def get_intersection(cls, edge1, edge2):
"""
-1
View File
@@ -36,7 +36,6 @@ class Collector(bonsai.core.tool.Collector):
# "Collection" (which is the default collection that comes with
# a Blender session)
if "Ifc" in users_collection.name or users_collection.name == "Collection":
print("removing", users_collection, "from", obj)
users_collection.objects.unlink(obj)
element = tool.Ifc.get_entity(obj)
+45
View File
@@ -807,3 +807,48 @@ class Cost(bonsai.core.tool.Cost):
return {
"Currency": currency,
}
@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"]
data = []
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects or []:
cls.create_cost_item_json(cost_item, cost_items, data)
return data
@classmethod
def create_cost_item_json(cls, cost_item: ifcopenshell.entity_instance, cost_items: dict, data: list):
if cost_item.id() in cost_items.keys():
cost_item_data = cost_items[cost_item.id()]
cost_item_data["id"] = cost_item.id()
cost_item_data["name"] = cost_item.Name
cost_item_data["is_nested_by"] = []
cost_item_data["is_sum"] = cls.is_cost_item_sum(cost_item)
data.append(cost_item_data)
else:
return None
for rel in cost_item.IsNestedBy or []:
for sub_cost_item in rel.RelatedObjects or []:
cls.create_cost_item_json(sub_cost_item, cost_items,cost_item_data["is_nested_by"])
@classmethod
def is_cost_item_sum(cls, cost_item: ifcopenshell.entity_instance) -> bool:
cost_values = []
if cost_item.is_a("IfcCostItem"):
cost_values = cost_item.CostValues
elif cost_item.is_a("IfcCostValue"):
cost_values = cost_item.Components
for cost_value in cost_values or []:
if cost_value.Category == "*":
return True
return False
@classmethod
def currency(cls):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
+4
View File
@@ -49,6 +49,8 @@ class Covering(bonsai.core.tool.Covering):
def covering_poll_wall_selected(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool:
if not tool.Ifc.get():
return False
if not context.selected_objects or not context.active_object:
operator.poll_message_set("No objects selected.")
return False
@@ -62,6 +64,8 @@ class Covering(bonsai.core.tool.Covering):
def covering_poll_relating_type_check(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool:
if not tool.Ifc.get():
return False
props = context.scene.BIMModelProperties
relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id")
if relating_type_id is not None:
+16 -7
View File
@@ -19,31 +19,40 @@
import os
import bpy
import ifcopenshell.express
import ifcopenshell.express.schema
import ifcopenshell.express.schema_class
import ifcopenshell.util.element
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from mathutils import Vector
class Debug(bonsai.core.tool.Debug):
@classmethod
def add_schema_identifier(cls, schema):
def add_schema_identifier(cls, schema: ifcopenshell.express.schema_class.SchemaClass) -> None:
IfcStore.schema_identifiers.append(schema.schema_name)
@classmethod
def load_express(cls, filename):
def load_express(cls, filename: str) -> ifcopenshell.express.schema_class.SchemaClass:
schema = ifcopenshell.express.parse(filename)
ifcopenshell.register_schema(schema)
return schema
@classmethod
def purge_hdf5_cache(cls):
def purge_hdf5_cache(cls) -> None:
cache_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache")
filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
for f in filelist:
os.remove(os.path.join(cache_dir, f))
try:
os.remove(os.path.join(cache_dir, f))
except PermissionError:
pass
@classmethod
def debug_geometry(cls, verts=[], edges=[], name="Debug"):
def debug_geometry(
cls, verts: list[Vector] = [], edges: list[tuple[int, int]] = [], name: str = "Debug"
) -> bpy.types.Object:
mesh = bpy.data.meshes.new("Debug")
mesh.from_pydata(verts, edges, [])
obj = bpy.data.objects.new(name, mesh)
@@ -51,13 +60,13 @@ class Debug(bonsai.core.tool.Debug):
return obj
@classmethod
def remove_unused_elements(cls, elements):
def remove_unused_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
ifc_file = tool.Ifc.get()
for element in elements:
ifcopenshell.util.element.remove_deep2(ifc_file, element)
@classmethod
def print_unused_elements_stats(cls, requested_ifc_class="", ignore_classes=tuple()):
def print_unused_elements_stats(cls, requested_ifc_class: str = "", ignore_classes: tuple[str] = tuple()) -> int:
ifc_file = tool.Ifc.get()
# get list of ifc classes used in model
+11 -12
View File
@@ -1669,20 +1669,17 @@ class Drawing(bonsai.core.tool.Drawing):
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
updated_set = set()
for i in elements:
# exclude annotations to avoid including annotations from other drawings
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
# add aggregate too, if element is host by one
if i.Decomposes:
aggregate = i.Decomposes[0].RelatingObject
if decomposes := i.Decomposes:
aggregate = decomposes[0].RelatingObject
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if not aggregate.is_a("IfcProject"):
if aggregate.is_a("IfcProduct"):
updated_set.add(aggregate)
# After the iteration is complete, update elements with updated set
elements.update(updated_set)
elements = updated_set
# add annotations from the current drawing
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
@@ -1782,6 +1779,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def activate_drawing(cls, camera: bpy.types.Object) -> None:
selected_objects_before = bpy.context.selected_objects
non_ifc_objects_hide = {o: o.hide_get() for o in bpy.context.view_layer.objects if not tool.Ifc.get_entity(o)}
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
@@ -1862,11 +1860,12 @@ class Drawing(bonsai.core.tool.Drawing):
element_obj_names.add(obj.name)
# Note that render visibility is only set on drawing generation time for speed.
[
obj.hide_set(False) # Show the object
for obj in bpy.context.view_layer.objects
if obj.name in element_obj_names or not tool.Ifc.get_entity(obj)
]
for obj in bpy.context.view_layer.objects:
if obj.name in element_obj_names:
obj.hide_set(False) # Show the object
continue
if (hide := non_ifc_objects_hide.get(obj)) is not None:
obj.hide_set(hide)
cls.import_camera_props(drawing, camera.data)
+91 -43
View File
@@ -41,7 +41,7 @@ class Raycast(bonsai.core.tool.Raycast):
return all_objs
@classmethod
def get_objects_2d_bounding_boxes(cls, context, obj):
def get_on_screen_2d_bounding_boxes(cls, context, obj):
obj_matrix = obj.matrix_world.copy()
bbox = [obj_matrix @ Vector(v) for v in obj.bound_box]
@@ -65,20 +65,28 @@ class Raycast(bonsai.core.tool.Raycast):
return (obj, bbox_2d)
@classmethod
def in_view_2d_bounding_box(cls, mouse_pos, bbox):
def intersect_mouse_2d_bounding_box(cls, mouse_pos, bbox, offset=None):
x, y = mouse_pos
xmin, xmax, ymin, ymax = bbox
# extends bbox boundaries to improve snap
if offset:
xmin -= offset
xmax += offset
ymin -= offset
ymax += offset
if xmin < x < xmax and ymin < y < ymax:
return True
else:
return False
@classmethod
def get_viewport_ray_data(cls, context, event):
def get_viewport_ray_data(cls, context, event, mouse_pos=None):
region = context.region
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
if not mouse_pos:
mouse_pos = event.mouse_region_x, event.mouse_region_y
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_pos)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_pos)
@@ -88,8 +96,11 @@ class Raycast(bonsai.core.tool.Raycast):
return ray_origin, ray_target, ray_direction
@classmethod
def get_object_ray_data(cls, context, event, obj_matrix):
ray_origin, ray_target, _ = cls.get_viewport_ray_data(context, event)
def get_object_ray_data(cls, context, event, obj_matrix, mouse_pos=None):
if mouse_pos:
ray_origin, ray_target, _ = cls.get_viewport_ray_data(context, event, mouse_pos)
else:
ray_origin, ray_target, _ = cls.get_viewport_ray_data(context, event)
matrix_inv = obj_matrix.inverted()
ray_origin_obj = matrix_inv @ ray_origin
ray_target_obj = matrix_inv @ ray_target
@@ -98,8 +109,13 @@ class Raycast(bonsai.core.tool.Raycast):
return ray_origin_obj, ray_target_obj, ray_direction_obj
@classmethod
def obj_ray_cast(cls, context, event, obj):
ray_origin_obj, _, ray_direction_obj = cls.get_object_ray_data(context, event, obj.matrix_world.copy())
def obj_ray_cast(cls, context, event, obj, mouse_pos=None):
if mouse_pos:
ray_origin_obj, _, ray_direction_obj = cls.get_object_ray_data(
context, event, obj.matrix_world.copy(), mouse_pos
)
else:
ray_origin_obj, _, ray_direction_obj = cls.get_object_ray_data(context, event, obj.matrix_world.copy())
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success:
return location, normal, face_index
@@ -107,7 +123,65 @@ class Raycast(bonsai.core.tool.Raycast):
return None, None, None
@classmethod
def ray_cast_by_proximity(cls, context, event, obj, mesh=None):
def ray_cast_by_proximity(cls, context, event, obj, face=None):
region = context.region
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
points = []
# Makes the snapping point more or less sticky then others
# It changes the distance and affects how the snapping point is sorted
stick_factor = 0.02
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
bm = bmesh.new()
if face is None: # Object with faces
bm.from_mesh(obj.data)
else: # Object without faces
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices]
bm.faces.new(verts)
for vertex in bm.verts:
world_vertex = obj.matrix_world.copy() @ vertex.co
intersection = tool.Cad.point_on_edge(world_vertex, (ray_target, loc))
distance = (world_vertex - intersection).length
if distance < 0.2:
points.append([distance - stick_factor, (world_vertex, "Vertex")])
for edge in bm.edges:
v1 = edge.verts[0].co
v2 = edge.verts[1].co
world_v1 = obj.matrix_world.copy() @ v1
world_v2 = obj.matrix_world.copy() @ v2
division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions
intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
distance = (division_point - intersection).length
if distance < 0.2:
points.append([distance, (division_point, "Edge Center")])
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (world_v1, world_v2))
if intersection:
if tool.Cad.is_point_on_edge(intersection[1], (world_v1, world_v2)):
distance = (intersection[1] - intersection[0]).length
if distance < 0.8:
points.append([distance + 4 * stick_factor, (intersection[1], "Edge")])
bm.free()
snapping_points = []
sorted_points = sorted(points)
for p in sorted_points:
snapping_points.append(p[1])
return snapping_points
@classmethod
def ray_cast_to_polyline(cls, context, event):
region = context.region
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
@@ -118,43 +192,17 @@ class Raycast(bonsai.core.tool.Raycast):
except:
loc = Vector((0, 0, 0))
bm = bmesh.new()
if mesh is None:
bm.from_mesh(obj.data)
else:
verts = [bm.verts.new(obj.data.vertices[i].co) for i in mesh.vertices]
bm.faces.new(verts)
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
polyline_points = []
for point_data in polyline_data:
point = Vector((point_data.x, point_data.y, point_data.z))
for edge in bm.edges:
v1 = edge.verts[0].co
v2 = edge.verts[1].co
world_v1 = obj.matrix_world @ v1
world_v2 = obj.matrix_world @ v2
division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions
intersection, _ = mathutils.geometry.intersect_point_line(division_point, ray_target, loc)
distance = (division_point - intersection).length
intersection, _ = mathutils.geometry.intersect_point_line(point, ray_target, loc)
distance = (point - intersection).length
if distance < 0.2:
return division_point, "Edge Center"
for vertex in bm.verts:
world_vertex = obj.matrix_world @ vertex.co
intersection, _ = mathutils.geometry.intersect_point_line(world_vertex, ray_target, loc)
distance = (world_vertex - intersection).length
if distance < 0.2:
return world_vertex, "Vertex"
for edge in bm.edges:
v1 = edge.verts[0].co
v2 = edge.verts[1].co
world_v1 = obj.matrix_world @ v1
world_v2 = obj.matrix_world @ v2
intersection = mathutils.geometry.intersect_line_line(ray_target, loc, world_v1, world_v2)
distance = (intersection[0] - intersection[1]).length
if distance < 0.2:
return intersection[1], "Edge"
return None, None
polyline_points.append((point, "Vertex"))
return polyline_points
@classmethod
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
+12 -7
View File
@@ -170,16 +170,21 @@ class Root(bonsai.core.tool.Root):
@classmethod
def is_containable(cls, element: ifcopenshell.entity_instance) -> bool:
if element.is_a("IfcElement") or element.is_a("IfcGrid"):
if element.is_a("IfcElement") or element.is_a("IfcGrid") or element.is_a("IfcAnnotation"):
return True
if element.is_a("IfcAnnotation"):
if element.ObjectType == "DRAWING":
return False
drawing_group = tool.Drawing.get_drawing_group(element)
if not drawing_group:
return True
return False
@classmethod
def is_drawing_annotation(cls, element: ifcopenshell.entity_instance) -> bool:
if not element.is_a("IfcAnnotation"):
return False
if element.ObjectType == "DRAWING":
return True
camera = bpy.context.scene.camera
if not camera or not tool.Ifc.get_entity(camera):
return False
return True
@classmethod
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return element.is_a(ifc_class)
+3 -2
View File
@@ -23,14 +23,15 @@ class Search(bonsai.core.tool.Search):
elif module == "diff":
return bpy.context.scene.DiffProperties.filter_groups
elif module == "drawing_include":
return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups
return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups
elif module == "drawing_exclude":
return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups
return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups
elif module.startswith("clash"):
_, clash_set_index, ab, clash_source_index = module.split("_")
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[
int(clash_source_index)
].filter_groups
assert False, f"Unsupported module: {module}"
@classmethod
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
+262 -117
View File
@@ -30,7 +30,8 @@ class Snap(bonsai.core.tool.Snap):
mouse_pos = None
snap_angle = None
use_default_container = False
snap_plane_method = "No Plane"
snap_plane_method = None
snap_axis_method = None
@classmethod
def set_use_default_container(cls, value=True):
@@ -40,6 +41,20 @@ class Snap(bonsai.core.tool.Snap):
def set_snap_plane_method(cls, value=True):
cls.snap_plane_method = value
@classmethod
def cycle_snap_plane_method(cls, value=True):
if cls.snap_plane_method == value:
cls.snap_plane_method = None
return
cls.snap_plane_method = value
@classmethod
def set_snap_axis_method(cls, value=True):
if cls.snap_axis_method == value:
cls.snap_axis_method = None
return
cls.snap_axis_method = value
@classmethod
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
matrix = obj.matrix_world.copy()
@@ -89,19 +104,24 @@ class Snap(bonsai.core.tool.Snap):
return snap_point
@classmethod
def update_snaping_point(cls, snap_point, snap_type):
def update_snapping_point(cls, snap_point, snap_type):
try:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0]
except:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point.add()
info = f"""Snap: {snap_type}
Axis:{cls.snap_axis_method}
Plane:{cls.snap_plane_method}
"""
PolylineDecorator.set_snap_info(info)
snap_vertex.x = snap_point[0]
snap_vertex.y = snap_point[1]
snap_vertex.z = snap_point[2]
snap_vertex.snap_type = snap_type
@classmethod
def update_snaping_ref(cls, snap_point, snap_type):
def update_snapping_ref(cls, snap_point, snap_type):
try:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_ref[0]
except:
@@ -113,7 +133,7 @@ class Snap(bonsai.core.tool.Snap):
snap_vertex.snap_type = snap_type
@classmethod
def clear_snaping_ref(cls):
def clear_snapping_ref(cls):
bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear()
@classmethod
@@ -140,7 +160,7 @@ class Snap(bonsai.core.tool.Snap):
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
if polyline_data:
last_point = polyline_data[len(polyline_data) - 1]
if (x, y, z) == (last_point.x, last_point.y, last_point.z):
if (x, y, z) == (round(last_point.x, 4), round(last_point.y, 4), round(last_point.z, 4)):
return
polyline_point = bpy.context.scene.BIMModelProperties.polyline_point.add()
@@ -181,7 +201,7 @@ class Snap(bonsai.core.tool.Snap):
def create_axis_line_data(rot_mat, origin):
length = 1000
direction = Vector((1, 0, 0))
if cls.snap_plane_method == "YZ":
if cls.snap_plane_method == "YZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"):
direction = Vector((0, 0, 1))
rot_dir = rot_mat.inverted() @ direction
start = origin + rot_dir * length
@@ -233,8 +253,6 @@ class Snap(bonsai.core.tool.Snap):
if cls.snap_plane_method == "YZ":
pivot_axis = "X"
rectangle_data = create_axis_rectangle_data(last_point)
PolylineDecorator.set_axis_rectangle(rectangle_data)
for axis in snap_axis:
rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis)
start, end = create_axis_line_data(rot_mat, last_point)
@@ -260,47 +278,27 @@ class Snap(bonsai.core.tool.Snap):
return None, None, None, None
@classmethod
def mix_snap_and_axis(cls, snap_point, axis_start, axis_end, elevation):
def mix_snap_and_axis(cls, snap_point, axis_start, axis_end):
# Creates a mixed snap point between the locked axis and
# the object snap
# TODO Use ALT key to give the user the option to choose between the two results.
# TODO Create decorator for this
snap_point_vector = Vector((snap_point[0].x, snap_point[0].y, snap_point[0].z))
snap_point_axis_1 = (
Vector((snap_point[0].x + 1000, snap_point[0].y, elevation)),
Vector((snap_point[0].x - 1000, snap_point[0].y, elevation)),
)
snap_point_axis_2 = (
Vector((snap_point[0].x, snap_point[0].y + 1000, elevation)),
Vector((snap_point[0].x, snap_point[0].y - 1000, elevation)),
)
snap_angle_axis = (axis_start, axis_end)
result_1 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_1)
result_1 = Vector((result_1[0].x, result_1[0].y, elevation))
distance_1 = (result_1 - snap_point_vector).length
result_2 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_2)
result_2 = Vector((result_2[0].x, result_2[0].y, elevation))
distance_2 = (result_2 - snap_point_vector).length
if distance_1 < distance_2:
best_result = result_1
else:
best_result = result_2
return best_result, "Mix"
# except Exception as e:
# cls.update_snaping_point(snap_point[0], snap_point[1])
intersections = []
intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((1, 0, 0))))
intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 1, 0))))
intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 0, 1))))
sorted_intersections = sorted(i for i in intersections if i is not None)
if sorted_intersections[0]:
return sorted_intersections[0], "Mix"
@classmethod
def snaping_movement(cls, context, event, objs_2d_bbox):
def detect_snapping_points(cls, context, event, objs_2d_bbox):
region = context.region
rv3d = context.region_data
space = context.space_data
cls.mouse_pos = event.mouse_region_x, event.mouse_region_y
detected_snaps = []
offset = 15
snap_threshold = 0.3
offset = 10
mouse_offset = (
(-offset, offset),
(0, offset),
@@ -316,13 +314,15 @@ class Snap(bonsai.core.tool.Snap):
def select_plane_method():
if not last_polyline_point:
plane_origin = Vector((0, 0, 0))
plane_normal = Vector((0, 0, 1))
if cls.snap_plane_method == "No Plane":
if not cls.snap_plane_method:
camera_rotation = rv3d.view_rotation
plane_origin = Vector((0, 0, 0))
plane_normal = Vector((0, 1, 1, 1)) * Vector((camera_rotation))
view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed()
plane_normal = view_direction.normalized()
if cls.snap_plane_method == "XY":
if cls.snap_plane_method == "XY" or (not cls.snap_plane_method and cls.snap_axis_method in {"X", "Y"}):
if cls.use_default_container:
plane_origin = Vector((0, 0, elevation))
elif not last_polyline_point:
@@ -331,7 +331,7 @@ class Snap(bonsai.core.tool.Snap):
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 0, 1))
elif cls.snap_plane_method == "XZ":
elif cls.snap_plane_method == "XZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"):
if last_polyline_point:
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 1, 0))
@@ -356,7 +356,7 @@ class Snap(bonsai.core.tool.Snap):
original_mouse_pos = cls.mouse_pos
for value in mouse_offset:
cls.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value))
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, cls.mouse_pos)
if hit:
break
cls.mouse_pos = original_mouse_pos
@@ -370,18 +370,49 @@ class Snap(bonsai.core.tool.Snap):
best_hit = hit_world
best_face_index = face_index
if best_obj is not None:
return best_obj, best_hit, best_face_index
else:
return None, None, None
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
if tool.Raycast.intersect_mouse_2d_bounding_box(cls.mouse_pos, bbox_2d, offset):
if space.local_view:
if obj.local_view_get(context.space_data):
objs_to_raycast.append(obj)
else:
objs_to_raycast.append(obj)
# Obj
snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast)
if hit is not None:
detected_snaps.append({"Object": (snap_obj, hit, face_index)})
# Edge-Vertex
for obj in objs_to_raycast:
if len(obj.data.polygons) == 0:
options = tool.Raycast.ray_cast_by_proximity(context, event, obj)
snap_obj = obj
if options:
detected_snaps.append({"Edge-Vertex": (snap_obj, options)})
break
# Polyline
try:
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
last_polyline_point = polyline_data[len(polyline_data) - 1]
except:
last_polyline_point = None
snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
# snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold)
if snap_points:
detected_snaps.append({"Polyline": snap_points})
# Axis and Plane
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin, plane_normal = select_plane_method()
@@ -390,75 +421,139 @@ class Snap(bonsai.core.tool.Snap):
axis_start = None
axis_end = None
if cls.snap_plane_method in {"XY", "XZ", "YZ"}:
# Locks snap into an angle axis
if event.shift:
# TODO It only work for XY plane. Make it work also for None plane_method
rot_intersection = None
if not cls.snap_plane_method:
if cls.snap_axis_method == "X":
cls.snap_angle = 180
if cls.snap_axis_method == "Y":
cls.snap_angle = 90
if cls.snap_axis_method == "Z":
cls.snap_angle = 90
if cls.snap_axis_method:
rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
if cls.snap_plane_method:
if cls.snap_plane_method in {"XY", "XZ"} and cls.snap_axis_method == "X":
cls.snap_angle = 180
if cls.snap_plane_method in {"XY", "YZ"} and cls.snap_axis_method == "Y":
cls.snap_angle = 90
if cls.snap_plane_method in {"YZ"} and cls.snap_axis_method == "Z":
cls.snap_angle = 180
if cls.snap_plane_method in {"XZ"} and cls.snap_axis_method == "Z":
cls.snap_angle = 90
if event.shift or cls.snap_axis_method:
# Doesn't update snap_angle so that it keeps in the same axis
rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
else:
cls.snap_angle = None
rot_intersection, cls.snap_angle, _, _ = cls.snap_on_axis(intersection)
else:
rot_intersection = None
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d):
objs_to_raycast.append(obj)
# Try to get object using object raycast method
# If it fails use the raycast by proximity
snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast)
if snap_obj is None:
for obj in objs_to_raycast:
hit, hit_type = tool.Raycast.ray_cast_by_proximity(context, event, obj)
snap_obj = obj
if hit is not None:
break
if snap_obj is not None:
# Objects with faces
if face_index is not None:
snap_point = cls.get_snap_points_on_raycasted_face(context, event, snap_obj, face_index)
if snap_point[0] is None:
cls.update_snaping_point(hit, "Face")
return
# Objects with only edges or verts
else:
if hit is not None:
snap_point = (hit, hit_type)
else:
snap_point = None
# If there are no objects to snap to, it will try snaping in the polyline
else:
snap_points = cls.get_snap_points_on_polyline()
snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold)
if snap_point:
if event.shift and axis_start:
snap_result, snap_type = cls.mix_snap_and_axis(snap_point, axis_start, axis_end, elevation)
cls.update_snaping_point(snap_result, snap_type)
cls.update_snaping_ref(snap_point[0], snap_point[1])
return
else:
cls.update_snaping_point(snap_point[0], snap_point[1])
return
# If theres no snapping point from objects and polyline
# It will snap to the plane
rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None)
if rot_intersection:
cls.update_snaping_point(rot_intersection, "Axis")
else:
cls.update_snaping_point(intersection, "Plane")
detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)})
detected_snaps.append({"Plane": intersection})
return detected_snaps
@classmethod
def validate_input(cls, input_number):
grammar = """
start: dim expr?
dim: NUMBER
expr: (ADD | SUB | MUL | DIV) NUMBER
def select_snapping_points(cls, context, event, detected_snaps):
snapping_points = []
for origin in detected_snaps:
if "Object" in list(origin.keys()):
snap_obj, hit, face_index = origin["Object"]
matrix = snap_obj.matrix_world.copy()
face = snap_obj.data.polygons[face_index]
verts = []
for i in face.vertices:
verts.append(matrix @ snap_obj.data.vertices[i].co)
options = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj, face)
if not options:
snapping_points.append((hit, "Face"))
else:
for op in options:
snapping_points.append(op)
break
if "Edge-Vertex" in list(origin.keys()):
snap_obj, options = origin["Edge-Vertex"]
for op in options:
snapping_points.append(op)
break
if "Polyline" in list(origin.keys()):
options = origin["Polyline"]
for op in options:
snapping_points.append(op)
break
if "Plane" in list(origin.keys()):
intersection = origin["Plane"]
snapping_points.append((intersection, "Plane"))
for origin in detected_snaps:
if "Axis" in list(origin.keys()):
intersection = origin["Axis"]
axis_start = intersection[1]
axis_end = intersection[2]
snapping_points.append((intersection[0], "Axis"))
# Make Axis first priority
if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}:
cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1])
for point in snapping_points:
if point[1] == "Axis":
if snapping_points[0][1] not in {"Axis", "Plane"}:
mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end)
cls.update_snapping_point(mixed_snap[0], mixed_snap[1])
return snapping_points
cls.update_snapping_point(point[0], point[1])
return snapping_points
cls.update_snapping_point(snapping_points[0][0], snapping_points[0][1])
return snapping_points
@classmethod
def modify_snapping_point_selection(cls, snapping_points):
shifted_list = snapping_points[1:] + snapping_points[:1]
cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1])
return shifted_list
@classmethod
def validate_input(cls, input_number, input_type):
grammar_imperial = """
start: FORMULA? dim expr?
dim: imperial
FORMULA: "="
imperial: feet? "-"? inches?
feet: NUMBER? " "? fraction? "'"
inches: NUMBER? " "? fraction? "\\""
fraction: NUMBER "/" NUMBER
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
NUMBER: /-?\\d+(?:\\.\\d+)?/
ADD: "+"
SUB: "-"
MUL: "*"
DIV: "/"
%ignore " "
"""
grammar_metric = """
start: FORMULA? dim expr?
dim: metric
FORMULA: "="
metric: NUMBER
expr: (ADD | SUB | MUL | DIV) dim
NUMBER: /-?\\d+(?:\\.\\d+)?/
ADD: "+"
@@ -470,8 +565,34 @@ class Snap(bonsai.core.tool.Snap):
"""
class InputTransform(Transformer):
def NUMBER(self, n):
return float(n)
def fraction(self, numbers):
return numbers[0] / numbers[1]
def inches(self, args):
if len(args) > 1:
result = args[0] + args[1]
else:
result = args[0]
return result / 12
def feet(self, args):
return args[0]
def imperial(self, args):
if len(args) > 1:
result = args[0] + args[1]
else:
result = args[0]
return result
def metric(self, args):
return args[0]
def dim(self, args):
return float(args[0])
return args[0]
def expr(self, args):
op = args[0]
@@ -485,17 +606,41 @@ class Snap(bonsai.core.tool.Snap):
elif op == "/":
return lambda x: x / value
def FORMULA(cls, args):
return args[0]
def start(self, args):
dimension = args[0]
if len(args) > 1:
expression = args[1]
return expression(dimension)
i = 0
if args[0] == "=":
i += 1
else:
return dimension
if len(args) > 1:
raise ValueError("Invalid input.")
dimension = args[i]
if len(args) > i + 1:
expression = args[i + 1]
return expression(dimension) * factor
else:
return dimension * factor
try:
parser = Lark(grammar, parser="lalr", transformer=InputTransform())
result = parser.parse(input_number)
if bpy.context.scene.unit_settings.system == "IMPERIAL":
parser = Lark(grammar_imperial)
factor = 0.3048
else:
parser = Lark(grammar_metric)
factor = 1
if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS":
factor = 0.001
if input_type == "A":
parser = Lark(grammar_metric)
factor = 1
parse_tree = parser.parse(input_number)
transformer = InputTransform()
result = transformer.transform(parse_tree)
return True, str(result)
except:
return False, "0"
+63 -15
View File
@@ -39,7 +39,8 @@ import json
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
from typing import Generator, Optional, Union, Literal, List
from typing import Generator, Optional, Union, Literal, List, Any, Iterable
from collections import defaultdict
class Spatial(bonsai.core.tool.Spatial):
@@ -139,7 +140,7 @@ class Spatial(bonsai.core.tool.Spatial):
target_obj.matrix_world = relative_to_obj.matrix_world @ matrix
@classmethod
def select_products(cls, products: list[ifcopenshell.entity_instance], unhide: bool = False) -> None:
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
bpy.ops.object.select_all(action="DESELECT")
for product in products:
obj = tool.Ifc.get_object(product)
@@ -206,7 +207,7 @@ class Spatial(bonsai.core.tool.Spatial):
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = {}
results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
@@ -218,25 +219,48 @@ class Spatial(bonsai.core.tool.Spatial):
ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0
type_name = element_type.Name or "Unnamed" if element_type else f"Untyped {element.is_a()}"
results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name})
results[ifc_class][ifc_definition_id]["total"] += 1
class_data = results.setdefault(ifc_class, {})
type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []})
type_data["elements"].append(element)
expanded_elements = json.loads(props.expanded_elements)
expanded_classes = expanded_elements.get("CLASS", [])
expanded_ifc_ids = expanded_elements.get("IFC_ID", [])
expanded_untyped = expanded_elements.get("UNTYPED_CLASSES", [])
total_elements = 0
for ifc_class in sorted(results.keys()):
new = props.elements.add()
new.name = ifc_class
new.is_class = True
new.type = "CLASS"
class_is_expanded = ifc_class in expanded_classes
new.is_expanded = class_is_expanded
total = 0
for ifc_definition_id in sorted(
results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"]
):
new2 = props.elements.add()
new2.is_type = True
new2.name = results[ifc_class][ifc_definition_id]["type_name"]
new2.ifc_class = ifc_class
new2.total = results[ifc_class][ifc_definition_id]["total"]
new2.ifc_definition_id = ifc_definition_id
total += new2.total
type_data = results[ifc_class][ifc_definition_id]
total2 = len(type_data["elements"])
if class_is_expanded:
new2 = props.elements.add()
new2.type = "TYPE"
new2.name = type_data["type_name"]
new2.ifc_class = ifc_class
new2.total = total2
new2.ifc_definition_id = ifc_definition_id
if ifc_definition_id == 0:
type_is_expanded = ifc_class in expanded_untyped
else:
type_is_expanded = ifc_definition_id in expanded_ifc_ids
new2.is_expanded = type_is_expanded
if type_is_expanded:
for element in type_data["elements"]:
occurrence = props.elements.add()
occurrence.name = element.Name or "Unnamed"
occurrence.ifc_definition_id = element.id()
occurrence.type = "OCCURRENCE"
total += total2
new.total = total
total_elements += total
@@ -248,11 +272,12 @@ class Spatial(bonsai.core.tool.Spatial):
elements: List[ifcopenshell.entity_instance],
ifc_class: str | None,
relating_type: ifcopenshell.entity_instance | None,
is_untyped: bool | None,
is_untyped: bool,
keyword: str | None,
) -> filter[ifcopenshell.entity_instance]:
keyword = keyword.lower() if keyword else keyword
def filter_element(element):
def filter_element(element: ifcopenshell.entity_instance) -> bool:
if ifc_class:
if not element.is_a(ifc_class):
return False
@@ -268,6 +293,7 @@ class Spatial(bonsai.core.tool.Spatial):
if keyword not in f"{element.is_a()} {type_name}".lower():
return False
return True
return filter(filter_element, elements)
@classmethod
@@ -336,6 +362,28 @@ class Spatial(bonsai.core.tool.Spatial):
contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def toggle_container_element(cls, element_index: int) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements)
element = props.elements[element_index]
if element.type == "CLASS":
element_type = "CLASS"
filtered_item = element.name
else:
if element.ifc_definition_id == 0:
element_type = "UNTYPED_CLASSES"
filtered_item = element.ifc_class
else:
element_type = "IFC_ID"
filtered_item = element.ifc_definition_id
expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault(element_type, [])
if filtered_item in expanded_elements_list:
expanded_elements_list.remove(filtered_item)
else:
expanded_elements_list.append(filtered_item)
props.expanded_elements = json.dumps(expanded_elements)
# HERE STARTS SPATIAL TOOL
@classmethod
+69 -21
View File
@@ -36,6 +36,8 @@ import queue
import json
from time import sleep
from pathlib import Path
import bonsai.core.sequence
import bonsai.core.cost
sio = None
ws_process = None
@@ -310,6 +312,8 @@ class Web(bonsai.core.tool.Web):
elif operator["sourcePage"] == "demo":
message = operator["operator"]["message"]
print(f"Message from demo page: {message}")
elif operator["sourcePage"] == "cost":
cls.handle_cost_operator(operator["operator"])
return 1.0
@classmethod
@@ -328,6 +332,48 @@ class Web(bonsai.core.tool.Web):
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
@classmethod
def handle_cost_operator(cls, operator_data: dict) -> None:
"""
this method handles the Cost page operators.
Args:
operator_data (dict): A dictionary containing the operator data.
"""
print("Handling cost operator")
ifc_file = tool.Ifc.get()
if operator_data["type"] == "getCostSchedules":
cost_schedules = ifc_file.by_type("IfcCostSchedule")
cost_schedules_json = [cs.get_info(recursive=True) for cs in cost_schedules]
currency = tool.Cost.currency()
cls.send_webui_data(data={
"cost_schedules": cost_schedules_json,
"currency": currency
}, data_key="cost_schedules", event="cost_schedules")
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")
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"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run(
"cost.edit_cost_item",
cost_item=cost_item,
attributes = {"Name": operator_data["name"]}
)
tool.Cost.load_cost_schedule_tree()
@classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None:
"""
@@ -337,34 +383,36 @@ class Web(bonsai.core.tool.Web):
operator_data (dict): A dictionary containing the operator data.
"""
ifc_file = tool.Ifc.get()
if operator_data["type"] == "getWorkSchedules":
work_schedules = ifc_file.by_type("IfcWorkSchedule")
work_schedules_json = [ws.get_info(recursive=True) for ws in work_schedules]
cls.send_webui_data(data=work_schedules_json, data_key="work_schedule_info", event="work_schedule_info")
if operator_data["type"] == "loadWorkSchedule":
work_schedule = ifc_file.by_id(operator_data["workScheduleId"])
print("Generating Gantt chart")
bonsai.core.sequence.enable_editing_work_schedule_tasks(tool.Sequence, work_schedule=work_schedule)
bonsai.core.sequence.generate_gantt_chart(tool.Sequence, work_schedule=work_schedule)
if operator_data["type"] == "editTask":
task_id = int(operator_data["taskId"])
task = ifc_file.by_id(task_id)
task_time = task.TaskTime
column = operator_data["column"]
new_value = operator_data["value"]
if task_time is None:
ifcopenshell.api.sequence.add_task_time(ifc_file, task)
task_time = task.TaskTime
ifcopenshell.api.sequence.edit_task_time(
ifc_file, task_time=task_time, attributes={IFC_TASK_ATTRIBUTE_MAP[column]: str(new_value)}
)
try:
ifcopenshell.api.sequence.edit_task(
ifc_file, task, attributes={IFC_TASK_ATTRIBUTE_MAP[column]: str(new_value)}
)
except AttributeError:
if task_time is None:
ifcopenshell.api.sequence.add_task_time(ifc_file, task)
task_time = task.TaskTime
ifcopenshell.api.sequence.edit_task_time(
ifc_file, task_time=task_time, attributes={IFC_TASK_ATTRIBUTE_MAP[column]: str(new_value)}
)
bpy.ops.bim.load_task_properties()
# after updating, send new gantt data to handle the case where
# changing a task cascades and changes other tasks. as this wouldn't
# be reflected in the web ui
work_schedule = ifc_file.by_id(operator_data["workScheduleId"])
task_json = tool.Sequence.create_tasks_json(work_schedule)
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
bpy.ops.bim.load_task_properties()
# after updating, send new gantt data to handle the case where
# changing a task cascades and changes other tasks. as this wouldn't
# be reflected in the web ui
work_schedule = ifc_file.by_id(operator_data["workScheduleId"])
task_json = tool.Sequence.create_tasks_json(work_schedule)
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
@classmethod
def handle_drawings_operator(cls, operator_data: dict) -> None:
+8
View File
@@ -83,6 +83,14 @@ img.icon {
font-size: small;
color: #808080;
}
span.menuselection {
border: 1px solid var(--color-admonition-title);
background-color: var(--color-admonition-title-background);
color: var(--color-admonition-text);
border-radius: 5px;
padding-left: 3px;
padding-right: 3px;
}
@@ -18,6 +18,30 @@ Models may be large in terms of different metrics, such as:
There are always solutions to all of these, but an understanding of the type of
size limitation you are up against will always help.
Linking in models
-----------------
Bonsai defaults to authoring IFCs. This allows full editing and inspection of
all element properties and relationships. However, sometimes only geometry and
basic attributes such as names are sufficient. Example usecases include CG
visualisation, overall federated model coordination, or pure geometric checks.
For these usecases, it's much more efficient to link a model rather than to
open a model. In the **Links Panel**, click on **Link IFC**, and browse to your
IFC. You can also bulk select multiple IFCs.
You will not be able to directly edit geometry or data in a linked model, but
you will be able to efficiently load multiple huge models easily and navigate
it with a reasonable FPS.
Activate the **Explore Tool** to quickly navigate these linked models. You can
**Enable Culling** to improve FPS speeds further, and use the :kbd:`RMB` to query
data about a selected object.
Once loaded, the linked model is cached as a ``.cache.blend`` file for
subsequent loads in the same folder as the ``.ifc``. The data is cached in a
``.cache.sqlite`` file.
Large filesizes
---------------
@@ -138,45 +162,6 @@ combined with other filters.
Using Blender 3.3 and above will result in a faster load time (~50%) compared to
older Blender versions.
Coordination only models
------------------------
Bonsai defaults to authoring IFCs. This allows full editing and inspection of
all element properties and relationships. However, sometimes only geometry and
basic attributes such as names are sufficient. Example usecases include CG
visualisation, overall federated model coordination, or pure geometric checks.
Click on :ref:`Enable Advanced Mode <Project Info Advanced Loading Mode>` checkbox when loading a model and you will be presented
with model loading options in the **Project Info** panel. Enable **For
Coordination Only**, which will exclude non geometric elements, openings, and
types from being imported. This leads to slightly faster imports, and a
decreased object count.
Enabling **For Coordination Only** also allows you to specify a **Merge Mode**.
This combines objects to keep object counts low. Blender is very good at
handling less objects with more complexity, rather than the other way around.
When a **Merge Mode** is activated, import times will increase (~50%) but object
counts will be drastically reduced, which is critical for the federation of
large models. **Merge Modes** include:
- **IFC Class**, where objects of the same IFC class are merged. This is useful
if you have models where only the class is meaningful for other disciplines,
such as structural models.
- **IFC Type**, where objects of the same construction type are merged. This is
useful where the main identification of interest is the element type, not the
element instance.
- **Material**, where objects of the same material are merged. This is useful if
the model is used for purely visual exploration such as CG visualisation.
Once loaded, the model may be saved as a ``.blend`` file for subsequent loads.
You can think of the ``.blend`` file as a geometry cache, which is very, very
fast to load. If it no longer necessary to access IFC data, consider pressing
the **Unload Project** icon so that future loads of the ``.blend`` file will be
very fast.
With these strategies, a federated 1GB IFC model can easily load in 10 seconds
from the saved Blender files.
Processing models headlessly
----------------------------
+6 -9
View File
@@ -52,15 +52,11 @@ and data-rich OpenBIM with Blender :)
:caption: Guides
:maxdepth: 2
guides/viewing/index
guides/authoring/index
guides/drawings/index
guides/structural_analysis/index
guides/services/index
guides/costing_and_scheduling/index
guides/facility_management/index
guides/coordination/index
guides/viewing/dealing_with_large_models
guides/authoring/georeferencing
guides/authoring/git_support
guides/development/index
guides/authoring/other_addons
guides/troubleshooting
.. toctree::
@@ -68,7 +64,8 @@ and data-rich OpenBIM with Blender :)
:caption: Reference
:maxdepth: 2
reference/workspace
reference/interface
reference/topbar
reference/properties
Need more help? Join the `live chat <https://osarch.org/chat/>`__ or `community
+21 -19
View File
@@ -16,21 +16,21 @@ you will need to categorise your 3D elements (such as "Wall", "Column",
Creating a single object
------------------------
In the **Properties** panel on the right, ensure the icon for the **Scene
Properties** tab is active showing the **Project Overview**. Click on **Create
Project** to create a blank IFC project.
Go to :menuselection:`Topbar --> File --> New IFC Project`, and select **New
Metric (m) Project**. This will begin a blank IFC4 project.
.. image:: images/create-project.png
In the left **Outliner** panel, you will see a hierarchy of spaces that has
been automatically created for you. This hierarchy is known is the **Spatial
Tree**.
In the :menuselection:`Properties --> Project Overview --> Spatial
Decomposition` panel, you will see a hierarchy of spaces that has been
automatically created for you. This hierarchy is known is the **Spatial Tree**.
.. image:: images/default-spatial-tree.png
There are many ways to create objects. In practice, you should use an element
type library, and we will see how to do this later. For now, we will only add a
single element. In the **Add** menu, add a cube.
single element. In the :menuselection:`3D Viewport --> Add --> Mesh` menu, select
**Cube**.
.. image:: images/add-cube.png
@@ -45,10 +45,10 @@ Class**.
Class**!
Select the cube (selected objects are highlighted in orange, careful not to
select anything else!) and switch to the **Object Information** tab. Let's
pretend our Cube is a column, so select **IfcElement** from the **Products**
dropdown, **IfcColumn** from the **Class** drop-down, and press **Assign IFC
Class**.
select anything else!) and switch to the :menuselection:`Properties --> Object
Information` tab. Let's pretend our Cube is a column, so select **IfcElement**
from the **Products** dropdown, **IfcColumn** from the **Class** drop-down, and
press **Assign IFC Class**.
.. image:: images/assign-class.png
@@ -58,19 +58,21 @@ Class**.
the shape of your object. You can have a monkey-shaped wall if you want!
All IFC objects must also belong inside the **Spatial Tree**. In the
**Outliner** panel, you will see that your newly created **IfcColumn/Cube** has
been automatically placed in **IfcBuildingStorey/My Storey**.
:menuselection:`Properties --> Object Information --> Spatial Container` panel,
you will see that your newly created **IfcColumn/Cube** has been automatically
placed in **IfcBuildingStorey/My Storey**.
.. image:: images/outliner-cube.png
.. image:: images/cube-spatial-tree.png
In the top left **File** menu, Save your new IFC model on your computer.
Go to :menuselection:`Topbar --> File` and click **Save IFC Project** to save
your new IFC model on your computer.
.. image:: images/save-project.png
Congratulations! You have now created your first OpenBIM model from Blender! You
can open the IFC file in any other program, and you will see something similar
to the image below. Three simple open source online viewers you can test with
are `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
Congratulations! You have now created your first IFC model with Bonsai! You can
open the IFC file in any other program, and you will see something similar to
the image below. Three simple open source online viewers you can test with are
`IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
<https://platform.thatopen.com/app>`__, and `3DViewer
<https://3dviewer.net/>`__.
+41 -37
View File
@@ -21,31 +21,33 @@ use some creativity when reading the data :)
Loading a model
---------------
Blender's interface is divided into three panels. The left **Outliner** panel
shows a tree of geometric objects. The centre main **Viewport** panel shows 3D
geometry. The right **Properties** panel shows data and relationships.
Bonsai's interface is divided into three panels. The left
:menuselection:`Outliner` panel shows geometric objects. The centre main
:menuselection:`3D Viewport` panel shows 3D geometry. The right
:menuselection:`Properties` panel shows data and relationships.
.. image:: images/bonsai-layout.png
The **Properties** panel has tabs to switch between different types of
properties.
The :menuselection:`Properties` panel has tabs to switch between different
types of properties.
.. image:: images/properties-tabs.png
Click on ``File > Open IFC Project`` and browse to your ``.ifc`` file.
Go to :menuselection:`Topbar --> File`, click **Open IFC Project**, and browse
to your ``.ifc`` file.
.. image:: images/properties-loadproject.png
After loading, you will see the model appear in the **Viewport** panel.
After loading, you will see the model appear in the :menuselection:`3D Viewport`.
.. image:: images/example-project.png
Take a look at the **Project Info** subpanel. It shows the loaded filename, as
well as the **IFC Schema**. There are two commonly seen **IFC Schema**
versions: IFC2X3 and IFC4. Checking the **IFC Schema** is important because it
has an impact on what BIM data may be stored. IFC4 is the newer version and it
is recommended to use IFC4 models as it has significantly more BIM capabilities
compared to IFC2X3.
Take a look at the :menuselection:`Properties --> Project Overview --> Project
Info` panel. It shows the loaded filename, as well as the **IFC Schema**. There
are two commonly seen **IFC Schema** versions: IFC2X3 and IFC4. Checking the
**IFC Schema** is important because it has an impact on what BIM data may be
stored. IFC4 is the newer version and it is recommended to use IFC4 models as
it has significantly more BIM capabilities compared to IFC2X3.
.. tip::
@@ -57,10 +59,10 @@ Navigating a model in 3D
------------------------
To navigate, can use the **Navigate Gizmo** on the top right corner of the
**Viewport** panel. Click and drag on the coloured axes to **Orbit**, click and
drag on the magnifying glass to **Zoom**, and click and drag on the hand icon to
**Pan**. You can also click on the grid icon to switch between perspective and
orthographic view.
:menuselection:`3D Viewport`. Click and drag on the coloured axes to **Orbit**,
click and drag on the magnifying glass to **Zoom**, and click and drag on the
hand icon to **Pan**. You can also click on the grid icon to switch between
perspective and orthographic view.
To switch to a top view, front view, or side view, click the relevant axis on
the **Navigate Gizmo**.
@@ -68,42 +70,44 @@ the **Navigate Gizmo**.
.. image:: images/navigate-gizmo.png
You can also use your mouse to navigate. Hover your mouse over the **Viewport**
panel and click and drag the Middle Mouse Button (``MMB``) to **Orbit**. Scroll
the mousewheel to **Zoom**, and use ``Shift-MMB`` to **Pan**.
panel and click and drag the Middle Mouse Button (:kbd:`MMB`) to **Orbit**.
Scroll the mousewheel to **Zoom**, and use :kbd:`Shift-MMB` to **Pan**.
If you have a numpad, you can use the numpad keys to quickly switch to top,
front, or side view. Use ``7`` for top view, ``1`` for front view, and ``3`` for
side view.
front, or side view. Use :kbd:`7` for top view, :kbd:`1` for front view, and
:kbd:`3` for side view.
.. warning::
Blender's hotkeys are context sensitive. This means that a hotkey has a
different meaning depending on the panel your mouse cursor is hovering over.
If you press ``7`` to go to top view, make sure your mouse cursor is over the
**Viewport** panel. Be very careful where your mouse is, or you might press a
hotkey and it will have unintended consequences!
If you press :kbd:`7` to go to top view, make sure your mouse cursor is over the
:menuselection:`3D Viewport`. Be very careful where your mouse is, or you
might press a hotkey and it will have unintended consequences!
If you click on an object, such as a wall in the **Viewport** panel, you can
zoom to the selected object by clicking on ``View > Frame Selected``. The hotkey
is the ``.`` button on the numpad. After zooming into an element, when you
**Orbit** the 3D view will rotate around the center of that element.
If you click on an object, such as a wall in the :menuselection:`3D Viewport`,
you can zoom to the selected object by clicking on :menuselection:`3D Viewport
--> View` then **Frame Selected**. The hotkey is the :kbd:`.` button on the
numpad. After zooming into an element, when you **Orbit** the 3D view will
rotate around the center of that element.
You can also zoom to all objects in the project by clicking on ``View > Frame
All``.
You can also zoom to all objects in the project by clicking on
:menuselection:`3D Viewport --> View` then **Frame All**.
.. image:: images/frame-selected.png
Another good way to navigate is by flying or walking around similar to a video
game. Choose ``View > Navigation > Walk Navigation``, or use the ``Shift-```
hotkey (the backtick key is usually to the left of the number 1 on the
keyboard). With **Walk Navigation** enabled, use the ``WASD`` keys and the mouse
to move around like a video game. You can use the ``Shift`` key to switch
between moving fast and slow. If you scroll with the mousewheel, it will adjust
the speed that you move at.
game. Click on :menuselection:`3D Viewport --> Explore Tool`, then you can
press :kbd:`Shift-W` to activate **Walk Mode**. Use the :kbd:`WASD` keys and
the mouse to move around like a video game. You can use the :kbd:`Shift` key to
switch between moving fast and slow. If you scroll with the mousewheel, it will
adjust the speed that you move at.
.. image:: images/explore-tool.png
Sometimes, you want to look through objects. You can toggle **X-Ray Mode** by
pressing the button on the top right of the **Viewport** panel. The hotkey is
``Alt-Z``.
:kbd:`Alt-Z`.
.. image:: images/x-ray-mode.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+38
View File
@@ -0,0 +1,38 @@
Interface
=========
.. container:: location-scene
|location| Interface
.. |location| image:: /images/location-scene.svg
The Bonsai interface is split into 5 main sections.
.. image:: images/interface.png
1. :doc:`Topbar </reference/topbar>` - shows main application menus to open and save IFCs, import IFCs, as well as tabs to save and load customised panel layouts.
2. :doc:`Outliner Panel </reference/outliner>` - shows a list of all geometric objects loaded into your 3D view. Objects can be organised into collections for any purpose.
3. :doc:`3D Viewport Panel </reference/3d_viewport>` - shows a 3D view of your geometric objects.
4. :doc:`Properties Panel </reference/properties>` - shows properties about your project, non-geometric objects, and selected objects from the 3D view.
5. :doc:`Status Bar </reference/status_bar>` - shows useful hotkeys when a tool is active, statistics, and version information.
Panels, such as the :doc:`/reference/outliner`, :doc:`/reference/3d_viewport`, and :doc:`/reference/properties` can be customised. You can click the top left icon of any panel to change the type of panel.
.. image:: images/interface-panel.png
You can split, merge, or create new panels by clicking :kbd:`RMB` in-between panels.
.. image:: images/interface-split-merge.png
You can save panel layouts, or switch to another customised panel layout by clicking the tabs in the :doc:`/reference/topbar`. Bonsai's default layout is stored in the **BIM** tab.
.. image:: images/interface-tabs.png
The :doc:`Topbar </reference/topbar>` and :doc:`/reference/status_bar` cannot be customised.
.. seealso::
Bonsai's interface is a customised version of the default Blender
interface. Read more about `Blender Workspaces
<https://docs.blender.org/manual/en/latest/interface/window_system/workspaces.html>`__.
+19 -48
View File
@@ -3,58 +3,29 @@ Properties
.. container:: location-scene
|location| Scene Properties
|location| Properties
.. |location| image:: /images/location-scene.svg
Bonsai adds new functionality to the `Property Editor` -> `Scene` tab.
The properties panel shows information about the IFC model and the actively
selected IFC object in the 3D Viewport.
.. figure:: images/interface_property-editor_project-overview_start-up.png
:alt: Property editor on Blender start-up
Tab bar
-------
The property editor on Blender startup.
A horizontal row of icons are provided to switch between what type of
properties are shown. Alternatively, a dropdown list may also be used.
Most of these sub-tabs become available with a created or loaded IFC file.
Don't worry, the default Blender scene properties are still reachable under their own dedicated sub-tab.
.. image:: images/properties-tabs.png
.. figure:: images/interface_property-editor_icons.png
:alt: Overview over the added property sub-tabs by Bonsai
Overview over the added property sub-tabs by Bonsai.
1. Project Overview
2. Object Information
3. Geometry and Materials
4. Drawings and Documents
5. Services and Systems
6. Structural Analyses
7. Costing and Scheduling
8. Facility Management
9. Quality and Coordination
10. Blender Properties
11. Switch Tab
You can also select the needed panel via the drop-down menue.
.. figure:: images/interface_property-editor_panel-dropdown.png
:alt: Bonsai property editor sub-tabs drop-down menue
Switching between Bonsai property editor sub-tabs via the drop-down menue.
.. toctree::
:hidden:
:maxdepth: 1
:caption: Contents:
project_overview/index
object_information/index
geometry_and_materials/index
drawings_and_documents/index
services_and_systems/index
structural_analysis/index
costing_and_scheduling/index
facility_management/index
quality_and_coordination/index
blender
switch_tab
- **Project Overview**: Overall project information.
- **Object Information**: Simple properties and relationships about the actively selected object.
- **Geometry and Materials**: Geometric, material, and style data.
- **Drawings and Documents**: Drawings, sheets, schedules, and other documents.
- **Services and Systems**: Mechanical, electrical, hydraulic, fire systems, and building physics.
- **Structural Analysis**: Structural analytical models and analysis.
- **Costing and Scheduling**: Project staging, quantity take-off, cost schedules, work schedules and animation, and resource management.
- **Facility Management**: Facility management integration, Brickschema integration.
- **Quality and Coordination**: Model auditing and fixing, clash detection, BCF collaboration, and debugging.
- **Blender Properties**: (Only visible at the bottom of the dropdown menu) Shows default Blender panels for Blender users who do not want to see IFC information.
- **Switch Tab** (:kbd:`Ctrl` + :kbd:`Tab`): (Only visible at the end of the tab icons) Toggles between the two last active tabs.

Some files were not shown because too many files have changed in this diff Show More