Merge branch 'v0.8.0' into webui_updates
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 689 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 863 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
----------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>`__.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -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>`__.
|
||||
@@ -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.
|
||||
|
||||