Merge branch 'v0.8.0' into webui_updates

This commit is contained in:
Dion Moult
2024-08-29 09:55:41 +10:00
committed by GitHub
234 changed files with 121241 additions and 190358 deletions
+6
View File
@@ -142,6 +142,12 @@ jobs:
sverchok_zip="$(pwd)/dist/$(ls dist)" sverchok_zip="$(pwd)/dist/$(ls dist)"
blender --command extension install-file -r user_default $sverchok_zip blender --command extension install-file -r user_default $sverchok_zip
# Install Sun Position extension.
blender --online-mode --command extension sync
blender --online-mode --background --python-expr "import bpy; \
bpy.ops.extensions.package_install(repo_index=0, pkg_id='sun_position'); \
bpy.ops.preferences.addon_enable(module='bl_ext.blender_org.sun_position'); bpy.ops.wm.save_userpref()"
cd ../bonsai cd ../bonsai
pip install pytest-blender pip install pytest-blender
blender --background --python scripts/setup_pytest.py blender --background --python scripts/setup_pytest.py
+2 -2
View File
@@ -12,7 +12,7 @@ is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending
is possible at compile-time when using C++ and at run-time when using Python. is possible at compile-time when using C++ and at run-time when using Python.
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application
to convert IFC models to other formats), the BlenderBIM Add-on (an add-on to Blender providing a graphical IFC authoring platform), to convert IFC models to other formats), the ~~BlenderBIM~~ Bonsai Add-on (an add-on to Blender providing a graphical IFC authoring platform),
and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS. and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
For more information, see: For more information, see:
@@ -37,7 +37,7 @@ Contents
| Name | Description | License | Service | | Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- | | ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) | | bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) |
| blenderbim | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=blenderbim-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) | | ~~blenderbim~~ Bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=blenderbim-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) | | bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later | | ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) | | ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
+1 -1
View File
@@ -81,7 +81,7 @@ ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"] PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1"]
JSON_VERSION = "v3.6.1" JSON_VERSION = "v3.6.1"
OCE_VERSION = "0.18.3" OCE_VERSION = "0.18.3"
OCCT_VERSION = "7.7.1" OCCT_VERSION = "7.5.3"
BOOST_VERSION = "1.80.0" BOOST_VERSION = "1.80.0"
PCRE_VERSION = "8.41" PCRE_VERSION = "8.41"
LIBXML2_VERSION = "2.9.11" LIBXML2_VERSION = "2.9.11"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif endif
# Current build commit hash. # Current build commit hash.
OLD:=d51fa2c OLD:=7e6607a
.PHONY: bump .PHONY: bump
bump: bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -121,6 +121,20 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["demo_data"] = data blender_messages[sid]["demo_data"] = data
await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web") 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): async def schedules(request):
with open("templates/index.html", "r") as f: 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}) html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html") 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): async def sequencing(request):
with open("templates/gantt.html", "r") as f: 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("/", schedules)
app.router.add_get("/documentation", documentation) app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing) app.router.add_get("/sequencing", sequencing)
app.router.add_get("/costing", costing)
app.router.add_get("/demo", demo) app.router.add_get("/demo", demo)
# Add static files # 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 { :root {
--font-family: Arial, sans-serif; --font-family: Arial, sans-serif;
--base-font-size: 16px; --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 // keeps track of blenders connected in form of
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool // shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
const connectedClients = {}; const connectedClients = {};
@@ -36,9 +38,16 @@ function connectSocket() {
socket.on("blender_connect", handleBlenderConnect); socket.on("blender_connect", handleBlenderConnect);
socket.on("blender_disconnect", handleBlenderDisconnect); socket.on("blender_disconnect", handleBlenderDisconnect);
socket.on("connected_clients", handleConnectedClients); socket.on("connected_clients", handleConnectedClients);
socket.on("connect", handleWebConnect);
socket.on("theme_data", handleThemeData); socket.on("theme_data", handleThemeData);
socket.on("gantt_data", handleGanttData); socket.on("gantt_data", handleGanttData);
socket.on("default_data", handleDefaultData); 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 // Function to handle 'blender_connect' event
@@ -62,6 +71,7 @@ function handleBlenderDisconnect(blenderId) {
console.log("blender disconnected: ", blenderId); console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) { if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId]; delete connectedClients[blenderId];
removeWorkScheduleInfo(blenderId);
removeGanttElement(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) { function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"]; const blenderId = data["blenderId"];
console.log(data); console.log(data);
@@ -156,7 +183,7 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"]; const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"]; const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty); showWarning(blenderId, isDirty);
console.log(data); console.log('default data',data);
} }
// Function to add a new gantt with data and filename // Function to add a new gantt with data and filename
@@ -551,3 +578,33 @@ function toggleClientList() {
clientList.addClass("show"); 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> <li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a> <a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li> </li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li> <li>
<a href="/sequencing" <a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a ><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -27,7 +27,7 @@
<script> <script>
var SOCKET_PORT = {{port}}; var SOCKET_PORT = {{port}};
</script> </script>
<script defer src="./static/js/gantt.js"></script> <script type="module" defer src="./static/js/gantt.js"></script>
</head> </head>
<body> <body>
<nav class="no-print"> <nav class="no-print">
@@ -40,6 +40,11 @@
<li> <li>
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a> <a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li> </li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li> <li>
<a href="/sequencing" class="active" <a href="/sequencing" class="active"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a ><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
@@ -76,6 +81,7 @@
</button> </button>
<div id="client-list" class="no-print"></div> <div id="client-list" class="no-print"></div>
</div> </div>
<div id="work_schedules"></div>
<div id="container"></div> <div id="container"></div>
<footer> <footer>
<p>Bonsai Version: {{version}}</p> <p>Bonsai Version: {{version}}</p>
@@ -49,6 +49,11 @@
><i class="fa-solid fa-table"></i> Schedules</a ><i class="fa-solid fa-table"></i> Schedules</a
> >
</li> </li>
<li>
<a href="/costing"
><i class="fa-solid fa-exclamation-triangle"></i>Costing</a
>
</li>
<li> <li>
<a href="/sequencing" <a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a ><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
+27 -8
View File
@@ -117,17 +117,34 @@ class IfcStore:
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() 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") 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() cache_settings = ifcopenshell.geom.settings()
serializer_settings = ifcopenshell.geom.serializer_settings()
cache_preexists = cache_path.exists()
try: try:
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings) IfcStore.cache = ifcopenshell.geom.serializers.hdf5(
except: IfcStore.cache_path, cache_settings, serializer_settings
if os.path.exists(IfcStore.cache_path): )
os.remove(IfcStore.cache_path) if cache_preexists:
try: print(f"Successfully loaded existing cache: {cache_path.name}.")
IfcStore.cache = ifcopenshell.geom.serializers.hdf5(IfcStore.cache_path, cache_settings)
except:
return
else: 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
return IfcStore.cache return IfcStore.cache
@@ -135,6 +152,8 @@ class IfcStore:
def update_cache(): def update_cache():
if not IfcStore.cache: if not IfcStore.cache:
return return
assert IfcStore.cache_path
assert IfcStore.file
ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp
ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() 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") 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): class PurgeHdf5Cache(bpy.types.Operator):
bl_idname = "bim.purge_hdf5_cache" bl_idname = "bim.purge_hdf5_cache"
bl_label = "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): def execute(self, context):
core.purge_hdf5_cache(tool.Debug) core.purge_hdf5_cache(tool.Debug)
self.report({"INFO"}, "HDF5 cache purged.")
return {"FINISHED"} return {"FINISHED"}
@@ -31,6 +31,7 @@ classes = (
operator.LoadParentDocument, operator.LoadParentDocument,
operator.LoadProjectDocuments, operator.LoadProjectDocuments,
operator.RemoveDocument, operator.RemoveDocument,
operator.SelectDocumentObjects,
operator.UnassignDocument, operator.UnassignDocument,
prop.Document, prop.Document,
prop.BIMDocumentProperties, prop.BIMDocumentProperties,
@@ -20,6 +20,7 @@ import bpy
import json import json
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.util.attribute import ifcopenshell.util.attribute
import ifcopenshell.util.element
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.document as core import bonsai.core.document as core
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
@@ -149,3 +150,25 @@ class UnassignDocument(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if element: if element:
core.unassign_document(tool.Ifc, product=element, document=document) 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") row.operator("bim.disable_editing_document", text="", icon="CANCEL")
elif self.props.documents and self.props.active_document_index < len(self.props.documents): 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 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.enable_editing_document", text="", icon="GREASEPENCIL").document = ifc_definition_id
row.operator("bim.remove_document", text="", icon="X").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: if not isArea:
add_inches = bool(inches) or not suppress_zero_inches add_inches = bool(inches) or not suppress_zero_inches
tx_dist = "" tx_dist = ""
if feet: if feet is not None:
tx_dist += str(feet) + "'" tx_dist += str(feet) + "'"
if feet and add_inches: if feet and add_inches:
tx_dist += " - " tx_dist += " - "
@@ -1463,7 +1463,7 @@ class ActivateModel(bpy.types.Operator):
bl_idname = "bim.activate_model" bl_idname = "bim.activate_model"
bl_label = "Activate Model" bl_label = "Activate Model"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
bl_description = "Activates the model view" bl_description = "Activate the model view, hide all annotations"
def execute(self, context): def execute(self, context):
dprops = bpy.context.scene.DocProperties dprops = bpy.context.scene.DocProperties
@@ -1471,10 +1471,23 @@ class ActivateModel(bpy.types.Operator):
CutDecorator.uninstall() 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] = {} visibility_status: dict[bpy.types.Object, bool] = {}
for obj in bpy.data.objects: 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: if not bpy.app.background:
with context.temp_override(**tool.Blender.get_viewport_context()): with context.temp_override(**tool.Blender.get_viewport_context()):
@@ -1779,7 +1792,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if self.index: if self.index:
index = int(self.index) index = int(self.index)
else: 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) scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style)
bpy.ops.bim.save_drawing_styles_data() 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() bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"} 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 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 space = self.get_view_3d(context) # Do not remove. It is used in exec later
style = json.loads(self.drawing_style.raster_style) 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 # Differences in Blender versions mean result in failures here
print(f"Failed to set shading style {path} to {value}") 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.include_global_ids = []
self.exclude_global_ids = [] self.exclude_global_ids = []
for ifc_file in context.scene.DocProperties.ifc_files: 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: if self.drawing_style.exclude_query:
self.parse_filter_query("EXCLUDE", context) 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": if mode == "INCLUDE":
objects = context.scene.objects objects = context.scene.objects
elif mode == "EXCLUDE": elif mode == "EXCLUDE":
@@ -1927,7 +1940,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if global_id in self.exclude_global_ids: if global_id in self.exclude_global_ids:
obj.hide_viewport = True # Note: this breaks alt-H 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: for area in context.screen.areas:
if area.type != "VIEW_3D": if area.type != "VIEW_3D":
continue continue
@@ -1935,6 +1948,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator):
if space.type != "VIEW_3D": if space.type != "VIEW_3D":
continue continue
return space return space
assert False, "Space is not found."
class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator): 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() filter_mode: bpy.props.StringProperty()
def _execute(self, context): def _execute(self, context):
props = context.active_object.data.BIMCameraProperties
obj = bpy.context.scene.camera obj = bpy.context.scene.camera
assert obj
props = obj.data.BIMCameraProperties
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
pset = tool.Pset.get_element_pset(element, "EPset_Drawing") pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
if self.filter_mode == "INCLUDE": if self.filter_mode == "INCLUDE":
+7 -2
View File
@@ -259,8 +259,12 @@ def update_should_draw_decorations(self, context):
continue continue
tool.Drawing.update_text_value(obj) tool.Drawing.update_text_value(obj)
refresh_drawing_data() refresh_drawing_data()
if bpy.app.background:
return
decoration.DecorationsHandler.install(context) decoration.DecorationsHandler.install(context)
else: else:
if bpy.app.background:
return
decoration.DecorationsHandler.uninstall() decoration.DecorationsHandler.uninstall()
@@ -531,9 +535,10 @@ def get_relating_type_id(self, context):
def update_annotation_object_type(self, context): def update_annotation_object_type(self, context):
self.relating_type_id = "0" # Refresh enum items before changing property,
# changing enum doesn't trigger refresh by itself # otherwise it might map to the wrong item.
AnnotationData.is_loaded = False AnnotationData.is_loaded = False
self.relating_type_id = "0"
def update_sheet_data(self, context): def update_sheet_data(self, context):
+5 -11
View File
@@ -106,11 +106,7 @@ class BIM_PT_element_filters(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return ( return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
def draw(self, context): def draw(self, context):
if not ElementFiltersData.is_loaded: if not ElementFiltersData.is_loaded:
@@ -160,17 +156,15 @@ class BIM_PT_drawing_underlay(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return ( return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera))
context.scene.camera
and context.active_object
and hasattr(context.active_object.data, "BIMCameraProperties")
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
camera = context.scene.camera
assert camera
dprops = context.scene.DocProperties 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) drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles)
if not DrawingsData.is_loaded: if not DrawingsData.is_loaded:
@@ -184,6 +184,8 @@ def create_annotation():
create_annotation_occurrence(bpy.context) create_annotation_occurrence(bpy.context)
else: else:
object_type = props.object_type object_type = props.object_type
if not bpy.ops.bim.add_annotation.poll():
return
bpy.ops.bim.add_annotation( bpy.ops.bim.add_annotation(
object_type=object_type, data_type=tool.Drawing.ANNOTATION_TYPES_DATA[object_type][-1] 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.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post) bpy.app.handlers.load_post.append(handler.load_post)
workspace.load_custom_icons()
def unregister(): def unregister():
if not bpy.app.background: if not bpy.app.background:
@@ -241,3 +242,4 @@ def unregister():
bpy.app.handlers.load_post.remove(handler.load_post) 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_mesh_add.remove(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu) bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
workspace.unload_custom_icons()
+106 -33
View File
@@ -28,6 +28,7 @@ from bpy_extras import view3d_utils
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from typing import Union from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
def transparent_color(color, alpha=0.1): def transparent_color(color, alpha=0.1):
@@ -306,6 +307,8 @@ class PolylineDecorator:
angle_snap_mat = None angle_snap_mat = None
angle_snap_loc = None angle_snap_loc = None
use_default_container = False use_default_container = False
instructions = None
snap_info = None
@classmethod @classmethod
def install(cls, context): def install(cls, context):
@@ -313,6 +316,9 @@ class PolylineDecorator:
cls.uninstall() cls.uninstall()
handler = cls() 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_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.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True cls.is_installed = True
@@ -353,6 +359,14 @@ class PolylineDecorator:
cls.plane_origin = plane_origin cls.plane_origin = plane_origin
cls.plane_normal = plane_normal 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 @classmethod
def calculate_distance_and_angle(cls, context, is_input_on): def calculate_distance_and_angle(cls, context, is_input_on):
@@ -391,7 +405,7 @@ class PolylineDecorator:
else: else:
# Creates a fake "second to last" point away from the first point but in the same x axis # 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 # 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 distance = (snap_vector - last_point).length
if distance > 0: if distance > 0:
@@ -451,7 +465,6 @@ class PolylineDecorator:
cls.input_panel["AREA"] = str(round(area, 4)) cls.input_panel["AREA"] = str(round(area, 4))
return cls.input_panel return cls.input_panel
@classmethod @classmethod
def calculate_x_y_and_z(cls, context): def calculate_x_y_and_z(cls, context):
try: try:
@@ -472,7 +485,7 @@ class PolylineDecorator:
else: else:
# Creates a fake "second to last" point away from the first point but in the same x axis # 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 # 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"]) distance = float(cls.input_panel["D"])
@@ -510,7 +523,16 @@ class PolylineDecorator:
batch.draw(shader) batch.draw(shader)
def draw_input_panel(self, context): 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.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0 self.font_id = 0
blf.size(self.font_id, 12) blf.size(self.font_id, 12)
@@ -521,6 +543,17 @@ class PolylineDecorator:
offset = 20 offset = 20
new_line = 20 new_line = 20
for i, (key, value) in enumerate(self.input_panel.items()): 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()): if key not in list(texts.keys()):
continue continue
if key == self.input_type: if key == self.input_type:
@@ -528,36 +561,72 @@ class PolylineDecorator:
else: else:
blf.color(self.font_id, *color) 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.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): def draw_measurements(self, context):
region = context.region region = context.region
rv3d = region.data rv3d = region.data
measurement_prop = context.scene.BIMModelProperties.polyline_measurement measurement_prop = context.scene.BIMModelProperties.polyline_measurement
self.addon_prefs = tool.Blender.get_addon_preferences() self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0 self.font_id = 1
blf.size(self.font_id, 12) blf.size(self.font_id, 12)
blf.enable(self.font_id, blf.SHADOW) blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1) blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color) blf.color(self.font_id, *color)
for i in range(len(measurement_prop)): for i in range(len(measurement_prop)):
if i == 0: if i == 0:
continue 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) 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) 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.position(self.font_id, coords_angle[0], coords_angle[1], 0)
blf.draw(self.font_id, "a: " + measurement_prop[i].angle) 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): def __call__(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences() self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -588,14 +657,6 @@ class PolylineDecorator:
except: except:
ref_point = None 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 default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = [] projection_point = []
if self.use_default_container: if self.use_default_container:
@@ -608,7 +669,7 @@ class PolylineDecorator:
edges = [[0, 1]] edges = [[0, 1]]
self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges) 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_data = context.scene.BIMModelProperties.polyline_point
polyline_points = [] polyline_points = []
polyline_edges = [] polyline_edges = []
@@ -619,18 +680,6 @@ class PolylineDecorator:
for i in range(len(polyline_points) - 1): for i in range(len(polyline_points) - 1):
polyline_edges.append([i, i + 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 # Line for angle axis snap
if snap_prop.snap_type == "Axis": if snap_prop.snap_type == "Axis":
@@ -649,3 +698,27 @@ class PolylineDecorator:
for i in range(1, len(polyline_points) - 1): for i in range(1, len(polyline_points) - 1):
edges.append((0, i, i + 1)) edges.append((0, i, i + 1))
self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
# Mouse points
if snap_prop.snap_type in ["Face", "Plane"]:
self.draw_batch("POINTS", mouse_point, decorator_color_unselected)
else:
self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0))
if ref_point:
self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0))
# Line between last polyline point and mouse
edges = [[0, 1]]
if polyline_points:
if snap_prop.snap_type != "Plane" and projection_point:
self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges)
else:
self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges)
# Draw polyline with selected points
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_selected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges)
+72 -21
View File
@@ -294,7 +294,27 @@ class DrawPolylineWall(bpy.types.Operator):
self.action_count = 0 self.action_count = 0
self.visible_objs = [] self.visible_objs = []
self.objs_2d_bbox = [] 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_input = []
self.number_output = "" self.number_output = ""
self.number_is_negative = False self.number_is_negative = False
@@ -304,23 +324,32 @@ class DrawPolylineWall(bpy.types.Operator):
self.input_value_xy = [None, None] self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} self.input_panel = {"D": "", "A": "", "X": "", "Y": ""}
self.snap_angle = None 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): def recalculate_inputs(self, context):
if self.number_input: 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 self.input_panel[self.input_type] = self.number_output
if not is_valid: if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.") self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else: else:
if self.input_type in {"X", "Y"}: if self.input_type in {"X", "Y"}:
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}: elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
self.input_panel[self.input_type] = self.number_output else:
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() tool.Blender.update_viewport()
return is_valid
# TODO This is creating a hack in generate function from DumbWallGenerator # TODO This is creating a hack in generate function from DumbWallGenerator
# Come up with a better solution # Come up with a better solution
@@ -356,7 +385,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.is_input_on = False self.is_input_on = False
self.input_type = "OFF" self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref() tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport() tool.Blender.update_viewport()
else: else:
self.mousemove_count = 0 self.mousemove_count = 0
@@ -364,10 +393,11 @@ class DrawPolylineWall(bpy.types.Operator):
if self.mousemove_count == 2: if self.mousemove_count == 2:
self.objs_2d_bbox = [] self.objs_2d_bbox = []
for obj in self.visible_objs: 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: 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) PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -381,6 +411,14 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Snap.insert_polyline_point(self.input_panel) tool.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport() 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": if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline() tool.Snap.close_polyline()
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) 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) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() 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.recalculate_inputs(context)
self.is_input_on = True self.is_input_on = True
self.input_type = event.type self.input_type = event.type
self.number_input = [] self.number_input = []
self.input_panel[self.input_type] = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -447,8 +486,9 @@ class DrawPolylineWall(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
self.recalculate_inputs(context) is_valid = self.recalculate_inputs(context)
tool.Snap.insert_polyline_point(self.input_panel) if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False self.is_input_on = False
self.input_type = "OFF" self.input_type = "OFF"
self.number_input = [] self.number_input = []
@@ -456,6 +496,12 @@ class DrawPolylineWall(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() 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"}: if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
@@ -467,6 +513,7 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Blender.update_viewport() tool.Blender.update_viewport()
else: else:
if event.value == "RELEASE" and event.type in {"ESC"}: if event.value == "RELEASE" and event.type in {"ESC"}:
tool.Snap.set_snap_axis_method(None)
PolylineDecorator.uninstall() PolylineDecorator.uninstall()
tool.Snap.clear_polyline() tool.Snap.clear_polyline()
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -480,11 +527,13 @@ class DrawPolylineWall(bpy.types.Operator):
tool.Snap.set_use_default_container(True) tool.Snap.set_use_default_container(True)
PolylineDecorator.set_use_default_container(True) PolylineDecorator.set_use_default_container(True)
tool.Snap.set_snap_plane_method("XY") tool.Snap.set_snap_plane_method("XY")
PolylineDecorator.set_instructions(self.instructions)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context) self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs: 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))
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) PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -669,7 +718,7 @@ class DumbWallGenerator:
for stroke in layer.active_frame.strokes: for stroke in layer.active_frame.strokes:
if len(stroke.points) == 1: if len(stroke.points) == 1:
continue 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: if data:
strokes.append(data) strokes.append(data)
objs.append(data["obj"]) objs.append(data["obj"])
@@ -694,19 +743,21 @@ class DumbWallGenerator:
bpy.context.scene.grease_pencil.layers.remove(layer) bpy.context.scene.grease_pencil.layers.remove(layer)
return objs 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] direction = coords[1] - coords[0]
length = direction.length length = direction.length
if length < 0.1: if length < 0.1:
return return
data = {"coords": coords} data = {"coords": coords}
# Round to nearest 50mm (yes, metric for now) self.length = length
self.length = 0.05 * round(length / 0.05)
self.rotation = math.atan2(direction[1], direction[0]) self.rotation = math.atan2(direction[1], direction[0])
# Round to nearest 5 degrees if round:
nearest_degree = (math.pi / 180) * 5 # Round to nearest 50mm (yes, metric for now)
self.rotation = nearest_degree * round(self.rotation / nearest_degree) 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] self.location = coords[0]
data["obj"] = self.create_wall() data["obj"] = self.create_wall()
return data return data
+400 -99
View File
@@ -17,7 +17,9 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os import os
import sys
import bpy import bpy
import bpy.utils.previews
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.model as core import bonsai.core.model as core
from bonsai.bim.module.model.wall import DumbWallJoiner 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.system.data import PortData
from bonsai.bim.module.model.prop import get_ifc_class 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): class BimTool(WorkSpaceTool):
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT" bl_context_mode = "OBJECT"
bl_idname = "bim.bim_tool" bl_idname = "bim.bim_tool"
bl_label = "BIM Tool" bl_label = "BIM Tool"
bl_description = "Create and edit elements by construction class" 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) BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type)
def add_layout_hotkey_operator(layout, text, hotkey, description): MODIFIERS = {
modifiers = { "A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"),
"A": "EVENT_ALT", "C": ("EVENT_CTRL", "CTRL"),
"C": "EVENT_CTRL", "S": ("EVENT_SHIFT", ""),
"S": "EVENT_SHIFT", }
}
def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""):
modifier, key = hotkey.split("_") modifier, key = hotkey.split("_")
row = layout.row(align=True) try:
row.label(text="", icon=modifiers[modifier]) custom_icon = custom_icon_previews[text.upper().replace(" ", "_")].icon_id
row.label(text="", icon=f"EVENT_{key}") 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.hotkey = hotkey
op.description = description op.description = description
return op return op
@@ -260,14 +297,19 @@ class BimToolUI:
AuthoringData.load(ifc_element_type) AuthoringData.load(ifc_element_type)
if context.region.type == "TOOL_HEADER": if context.region.type == "TOOL_HEADER":
cls.draw_header_interface() cls.draw_container(context)
elif context.region.type in ("UI", "WINDOW"): cls.draw_type_selection_interface(context)
# same interface for both n-panel sidebar and object properties if context.active_object and context.selected_objects:
cls.draw_basic_bim_tool_interface() cls.draw_edit_object_header_interface(context)
if context.active_object and context.selected_objects: elif context.region.type in ("UI", "WINDOW"):
cls.draw_edit_object_interface(context) cls.draw_container(context)
elif not context.selected_objects: 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() cls.draw_create_object_interface()
@classmethod @classmethod
@@ -285,10 +327,10 @@ class BimToolUI:
row.prop(data=cls.props, property="length", text="Length") row.prop(data=cls.props, property="length", text="Length")
row = cls.layout.row(align=True) 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"): elif cls.props.ifc_class in ("IfcSlabType", "IfcRampType", "IfcRoofType"):
row = cls.layout.row(align=True) 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"): elif cls.props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"):
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.prop(data=cls.props, property="cardinal_point", text="Axis") 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") row.prop(data=cls.props, property="rl_mode", text="RL")
@classmethod @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": if AuthoringData.data["active_material_usage"] == "LAYER2":
row = cls.layout.row(align=True) 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 = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth op.depth = cls.props.extrusion_depth
row = cls.layout.row(align=True) 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 = row.operator("bim.change_layer_length", icon="FILE_REFRESH", text="")
op.length = cls.props.length op.length = cls.props.length
row = cls.layout.row(align=True) 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 = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle op.x_angle = cls.props.x_angle
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") cls.layout.separator()
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "")
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( add_layout_hotkey_operator(
cls.layout, cls.layout,
"Mitre", "Mitre",
"S_Y", "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__) row = cls.layout.row(align=True)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__) row.operator(
add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__) "bim.unjoin_walls", text="Unjoin Walls", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id
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__) cls.layout.separator()
row.operator("bim.unjoin_walls", icon="X", text="")
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": elif AuthoringData.data["active_material_usage"] == "LAYER3":
if len(context.selected_objects) == 1: 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"]: 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 = 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 = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle 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 = row.operator("bim.change_profile_depth", icon="FILE_REFRESH", text="")
op.depth = cls.props.extrusion_depth op.depth = cls.props.extrusion_depth
add_layout_hotkey_operator(cls.layout, "Extend", "S_E", "") add_layout_hotkey_operator(
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_object.__doc__) 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 ( if AuthoringData.data["active_class"] in (
"IfcCableCarrierSegment", "IfcCableCarrierSegment",
@@ -386,18 +460,37 @@ class BimToolUI:
"IfcPipeSegment", "IfcPipeSegment",
): ):
add_layout_hotkey_operator(cls.layout, "Add Fitting", "S_Y", "") 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_bend") cls.layout.operator("bim.mep_add_transition")
cls.layout.operator("bim.mep_add_transition") cls.layout.operator("bim.mep_add_obstruction")
cls.layout.operator("bim.mep_add_obstruction")
else: else:
add_layout_hotkey_operator(cls.layout, "Edit Axis", "A_E", "") add_layout_hotkey_operator(
add_layout_hotkey_operator(cls.layout, "Butt", "S_T", "") cls.layout,
add_layout_hotkey_operator(cls.layout, "Mitre", "S_Y", "") "Edit Axis",
add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__) "A_E",
add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_profile.__doc__) "",
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 = "" row.operator("bim.extend_profile", icon="X", text="").join_type = ""
elif ( elif (
@@ -422,15 +515,15 @@ class BimToolUI:
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.prop(data=cls.props, property="rl1", text="RL") 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, "Regen", "S_G", bpy.ops.bim.recalculate_fill.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "") add_layout_hotkey_operator(cls.layout, "Flip", "S_F", "", ui_context)
elif AuthoringData.data["active_representation_type"] == "SweptSolid": elif AuthoringData.data["active_representation_type"] == "SweptSolid":
if not tool.Model.is_parametric_window_active() and not tool.Model.is_parametric_door_active(): 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",): 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: elif tool.Model.is_parametric_roof_active() and not context.active_object.BIMRoofProperties.is_editing_path:
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
@@ -439,25 +532,25 @@ class BimToolUI:
if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0: if context.region.type != "TOOL_HEADER" and PortData.data["total_ports"] > 0:
add_layout_hotkey_operator( 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") cls.layout.operator("bim.mep_connect_elements")
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_O")
if len(context.selected_objects) > 1: 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: 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["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]: if AuthoringData.data["has_visible_openings"]:
row = cls.layout.row(align=True)
row.operator("bim.edit_openings", icon="CHECKMARK", text="") row.operator("bim.edit_openings", icon="CHECKMARK", text="")
row.operator("bim.hide_openings", icon="CANCEL", 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",): if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
row.operator("bim.edit_openings", icon="CHECKMARK", text="") row.operator("bim.edit_openings", icon="CHECKMARK", text="")
@@ -469,51 +562,264 @@ class BimToolUI:
row.operator("bim.clone_opening", text="Clone Opening") row.operator("bim.clone_opening", text="Clone Opening")
cls.layout.row(align=True).label(text="Align") cls.layout.row(align=True).label(text="Align")
add_layout_hotkey_operator(cls.layout, "Align Exterior", "S_X", "") add_layout_hotkey_operator(cls.layout, "Exterior", "S_X", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Align Centerline", "S_C", "") add_layout_hotkey_operator(cls.layout, "Centerline", "S_C", "", ui_context)
add_layout_hotkey_operator(cls.layout, "Align Interior", "S_V", "") 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__) 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") 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, "Void", "A_O", "Toggle openings", ui_context)
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition") add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition", ui_context)
cls.layout.row(align=True).label(text="Aggregation") 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, "Assign", "C_P", bpy.ops.bim.aggregate_assign_object.__doc__, ui_context)
add_layout_hotkey_operator(cls.layout, "Unassign", "A_P", bpy.ops.bim.aggregate_unassign_object.__doc__)
cls.layout.separator()
add_layout_hotkey_operator( 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 @classmethod
def draw_header_interface(cls): def draw_edit_object_header_interface(cls, context):
cls.draw_type_selection_interface() ui_context = str(context.region.type)
if AuthoringData.data["active_material_usage"] == "LAYER2":
if AuthoringData.data["ifc_classes"]:
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.prop(data=cls.props, property="extrusion_depth", text="Height:")
row.label(text="", icon="EVENT_A") op = row.operator("bim.change_extrusion_depth", icon="FILE_REFRESH", text="")
op = row.operator("bim.add_constr_type_instance", text="Add") op.depth = cls.props.extrusion_depth
op.from_invoke = True
if cls.props.relating_type_id.isnumeric(): row = cls.layout.row(align=True)
op.relating_type_id = int(cls.props.relating_type_id) 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 @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 # shared by both sidebar and header
ui_context = str(context.region.type)
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.label(text=f"Container: {AuthoringData.data['default_container']}", icon="OUTLINER_COLLECTION")
if AuthoringData.data["ifc_classes"]: if AuthoringData.data["ifc_classes"]:
if ui_context == "UI":
text = "Add"
else:
text = ""
if not AuthoringData.data["ifc_element_type"]: if not AuthoringData.data["ifc_element_type"]:
row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "ifc_class", text="") 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) row = cls.layout.row(align=True)
if AuthoringData.data["relating_type_id"]: if AuthoringData.data["relating_type_id"]:
row.label(text="", icon="FILE_3D")
prop_with_search(row, cls.props, "relating_type_id", text="") 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: else:
row.label(text="No Construction Type", icon="FILE_3D") row.label(text="No Construction Type", icon="FILE_3D")
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") 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.prop(cls.props, "type_name")
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
op = row.operator( 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"] op.ifc_element_type = AuthoringData.data["ifc_element_type"]
row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="") 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") row.operator("bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, text="Launch Type Manager")
@classmethod @classmethod
def draw_basic_bim_tool_interface(cls): def draw_thumbnail(cls):
cls.draw_type_selection_interface()
if AuthoringData.data["ifc_classes"]: if AuthoringData.data["ifc_classes"]:
if cls.props.ifc_class: if cls.props.ifc_class:
box = cls.layout.box() box = cls.layout.box()
@@ -545,18 +851,10 @@ class BimToolUI:
op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH") op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH")
op.ifc_class = cls.props.ifc_class 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): class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hotkey" bl_idname = "bim.hotkey"
bl_label = "Hotkey" bl_label = ""
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
hotkey: bpy.props.StringProperty() hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty() description: bpy.props.StringProperty()
@@ -843,7 +1141,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.z = self.z self.props.z = self.z
def hotkey_S_P(self): 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): def hotkey_S_L(self):
if AuthoringData.data["active_class"] in ("IfcOpeningElement",): if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
@@ -2303,7 +2303,27 @@ class MeasureTool(bpy.types.Operator):
self.action_count = 0 self.action_count = 0
self.visible_objs = [] self.visible_objs = []
self.objs_2d_bbox = [] 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_input = []
self.number_output = "" self.number_output = ""
self.number_is_negative = False self.number_is_negative = False
@@ -2313,23 +2333,33 @@ class MeasureTool(bpy.types.Operator):
self.input_value_xy = [None, None] self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None 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): def recalculate_inputs(self, context):
if self.number_input: 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 self.input_panel[self.input_type] = self.number_output
if not is_valid: if not is_valid:
self.report({"WARNING"}, "The number typed is not valid.") self.report({"WARNING"}, "The number typed is not valid.")
return is_valid
else: else:
if self.input_type in {"X", "Y", "Z"}: if self.input_type in {"X", "Y", "Z"}:
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}: elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() tool.Blender.update_viewport()
return is_valid
def modal(self, context, event): def modal(self, context, event):
@@ -2339,7 +2369,7 @@ class MeasureTool(bpy.types.Operator):
self.is_input_on = False self.is_input_on = False
self.input_type = "OFF" self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref() tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport() tool.Blender.update_viewport()
else: else:
self.mousemove_count = 0 self.mousemove_count = 0
@@ -2347,10 +2377,11 @@ class MeasureTool(bpy.types.Operator):
if self.mousemove_count == 2: if self.mousemove_count == 2:
self.objs_2d_bbox = [] self.objs_2d_bbox = []
for obj in self.visible_objs: 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: 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) PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) 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.Snap.insert_polyline_point(self.input_panel)
tool.Blender.update_viewport() 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": if event.value == "PRESS" and event.type == "C":
tool.Snap.close_polyline() tool.Snap.close_polyline()
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) 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) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() 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.recalculate_inputs(context)
self.is_input_on = True self.is_input_on = True
self.input_type = event.type self.input_type = event.type
@@ -2424,8 +2467,9 @@ class MeasureTool(bpy.types.Operator):
tool.Blender.update_viewport() tool.Blender.update_viewport()
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
self.recalculate_inputs(context) is_valid = self.recalculate_inputs(context)
tool.Snap.insert_polyline_point(self.input_panel) if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False self.is_input_on = False
self.input_type = "OFF" self.input_type = "OFF"
self.number_input = [] self.number_input = []
@@ -2433,20 +2477,32 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport() 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": if event.shift and event.value == "PRESS" and event.type == "X":
tool.Snap.set_use_default_container(False) tool.Snap.set_use_default_container(False)
PolylineDecorator.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": if event.shift and event.value == "PRESS" and event.type == "Y":
tool.Snap.set_use_default_container(False) tool.Snap.set_use_default_container(False)
PolylineDecorator.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": if event.shift and event.value == "PRESS" and event.type == "Z":
tool.Snap.set_use_default_container(False) tool.Snap.set_use_default_container(False)
PolylineDecorator.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"}: if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"} return {"PASS_THROUGH"}
@@ -2459,6 +2515,8 @@ class MeasureTool(bpy.types.Operator):
tool.Blender.update_viewport() tool.Blender.update_viewport()
else: else:
if event.value == "RELEASE" and event.type in {"ESC"}: 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() PolylineDecorator.uninstall()
tool.Snap.clear_polyline() tool.Snap.clear_polyline()
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -2471,12 +2529,15 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.install(context) PolylineDecorator.install(context)
tool.Snap.set_use_default_container(False) tool.Snap.set_use_default_container(False)
PolylineDecorator.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) PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
self.visible_objs = tool.Raycast.get_visible_objects(context) self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs: 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))
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) PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport() tool.Blender.update_viewport()
+4 -3
View File
@@ -84,12 +84,13 @@ class ColourByPropertyData:
@classmethod @classmethod
def colourscheme_key(cls): def colourscheme_key(cls):
default = [("QUERY", "Custom Query", "Specify a custom query to colour by"), None]
obj = bpy.context.active_object obj = bpy.context.active_object
if not obj: if not obj:
return [] return default
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
return [] return default
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()] keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element) psets = ifcopenshell.util.element.get_psets(element)
for pset, properties in psets.items(): for pset, properties in psets.items():
@@ -100,7 +101,7 @@ class ColourByPropertyData:
else: else:
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"]) keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
results = [(k, k, "") for k in keys] 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: class SelectSimilarData:
@@ -124,6 +124,7 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_description = "Edit the underlying filter query for advanced users" bl_description = "Edit the underlying filter query for advanced users"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query") query: StringProperty(name="Query")
old_query: StringProperty(name="Old Query")
module: StringProperty() module: StringProperty()
def _execute(self, context): def _execute(self, context):
@@ -38,6 +38,7 @@ classes = (
operator.SelectSimilarContainer, operator.SelectSimilarContainer,
operator.SetContainerVisibility, operator.SetContainerVisibility,
operator.SetDefaultContainer, operator.SetDefaultContainer,
operator.ToggleContainerElement,
prop.Element, prop.Element,
prop.BIMObjectSpatialProperties, prop.BIMObjectSpatialProperties,
prop.BIMContainer, 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)) 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): class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_decomposed_elements" bl_idname = "bim.select_decomposed_elements"
bl_label = "Select Children" bl_label = "Select Children"
@@ -260,25 +271,33 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context) return self.execute(context)
def _execute(self, 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: if self.should_filter:
active_element = context.scene.BIMSpatialDecompositionProperties.active_element 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 ifc_class = active_element.name
elif relating_type := active_element.ifc_definition_id: elif element_type == "TYPE":
ifc_class = active_element.ifc_class ifc_class = active_element.ifc_class
relating_type = tool.Ifc.get().by_id(relating_type) if ifc_id := active_element.ifc_definition_id:
else: relating_type = ifc_file.by_id(ifc_id)
ifc_class = active_element.ifc_class else: # OCCURRENCE
is_untyped = True 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 element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter
core.select_decomposed_elements( core.select_decomposed_elements(
tool.Spatial, tool.Spatial,
container=tool.Ifc.get().by_id(self.container), container=ifc_file.by_id(self.container),
ifc_class=ifc_class, ifc_class=ifc_class,
relating_type=relating_type, relating_type=relating_type,
is_untyped=is_untyped, is_untyped=is_untyped,
element_filter = element_filter element_filter=element_filter,
) )
+15 -5
View File
@@ -107,21 +107,31 @@ class BIMContainer(PropertyGroup):
class Element(PropertyGroup): class Element(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
ifc_class: StringProperty(name="Name") ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'")
is_class: BoolProperty(name="Is Class", default=False) ifc_definition_id: IntProperty(
is_type: BoolProperty(name="Is Type", default=False) name="IFC Definition ID",
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") 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): class BIMSpatialDecompositionProperties(PropertyGroup):
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"}) container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
containers: CollectionProperty(name="Containers", type=BIMContainer) containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]") 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) active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"}) element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"})
elements: CollectionProperty(name="Elements", type=Element) elements: CollectionProperty(name="Elements", type=Element)
expanded_elements: StringProperty(name="Expanded Elements", default="{}")
active_element_index: IntProperty(name="Active Element Index") active_element_index: IntProperty(name="Active Element Index")
total_elements: IntProperty(name="Total Elements") total_elements: IntProperty(name="Total Elements")
subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class") subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class")
+14 -4
View File
@@ -258,21 +258,31 @@ class BIM_UL_elements(UIList):
def __init__(self): def __init__(self):
self.use_filter_show = True 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: if item:
row = layout.row(align=True) row = layout.row(align=True)
if item.is_class: item_type = item.type
row.label(text="", icon="DISCLOSURE_TRI_DOWN") if item_type == "CLASS":
self.draw_toggle(row, item.is_expanded, index)
row.label(text=item.name) row.label(text=item.name)
col = row.column() col = row.column()
col.alignment = "RIGHT" col.alignment = "RIGHT"
col.label(text=str(item.total)) col.label(text=str(item.total))
elif item.is_type: elif item_type == "TYPE":
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
self.draw_toggle(row, item.is_expanded, index)
row.label(text=item.name) row.label(text=item.name)
col = row.column() col = row.column()
col.alignment = "RIGHT" col.alignment = "RIGHT"
col.label(text=str(item.total)) 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): def draw_filter(self, context, layout):
row = layout.row() row = layout.row()
+12 -3
View File
@@ -17,15 +17,24 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>. # 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)) 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() 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() 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 = [i for i in ifc_file.by_type(ifc_class) if ifc_file.get_total_inverses(i) == 0]
unused_elements_amount = len(unused_elements) unused_elements_amount = len(unused_elements)
+1
View File
@@ -122,6 +122,7 @@ def switch_representation(
return return
entity = ifc.get_entity(obj) entity = ifc.get_entity(obj)
assert entity
current_obj_data = geometry.get_object_data(obj) current_obj_data = geometry.get_object_data(obj)
if not current_obj_data and geometry.is_text_literal(representation): if not current_obj_data and geometry.is_text_literal(representation):
+1 -1
View File
@@ -86,7 +86,7 @@ def assign_class(
obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None 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): if root.is_spatial_element(element):
ifc.run("aggregate.assign_object", products=[element], relating_object=default_container) ifc.run("aggregate.assign_object", products=[element], relating_object=default_container)
elif root.is_containable(element): elif root.is_containable(element):
+5
View File
@@ -141,6 +141,11 @@ def delete_container(
spatial.import_spatial_decomposition() 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( def select_decomposed_elements(
spatial: tool.Spatial, spatial: tool.Spatial,
container: ifcopenshell.entity_instance, container: ifcopenshell.entity_instance,
+1
View File
@@ -723,6 +723,7 @@ class Root:
def get_object_representation(cls, obj): pass def get_object_representation(cls, obj): pass
def get_representation_context(cls, representation): pass def get_representation_context(cls, representation): pass
def is_containable(cls, element): 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_element_a(cls, element, ifc_class): pass
def is_spatial_element(cls, element): pass def is_spatial_element(cls, element): pass
def link_object_data(cls, source_obj, destination_obj): pass def link_object_data(cls, source_obj, destination_obj): pass
+46
View File
@@ -142,6 +142,14 @@ class Cad:
def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float | None = None) -> bool: def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float | None = None) -> bool:
return cls.is_x((v2 - v1).length, 0, tolerance) 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 @classmethod
def intersect_edges(cls, edge1, edge2): 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 r1.to_2d() if r1 else r1, r2.to_2d() if r2 else r2
return results 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 @classmethod
def get_intersection(cls, edge1, edge2): def get_intersection(cls, edge1, edge2):
""" """
-1
View File
@@ -36,7 +36,6 @@ class Collector(bonsai.core.tool.Collector):
# "Collection" (which is the default collection that comes with # "Collection" (which is the default collection that comes with
# a Blender session) # a Blender session)
if "Ifc" in users_collection.name or users_collection.name == "Collection": if "Ifc" in users_collection.name or users_collection.name == "Collection":
print("removing", users_collection, "from", obj)
users_collection.objects.unlink(obj) users_collection.objects.unlink(obj)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
+45
View File
@@ -807,3 +807,48 @@ class Cost(bonsai.core.tool.Cost):
return { return {
"Currency": currency, "Currency": currency,
} }
@classmethod
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
from bonsai.bim.module.cost.data import CostSchedulesData
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
data = []
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects or []:
cls.create_cost_item_json(cost_item, cost_items, data)
return data
@classmethod
def create_cost_item_json(cls, cost_item: ifcopenshell.entity_instance, cost_items: dict, data: list):
if cost_item.id() in cost_items.keys():
cost_item_data = cost_items[cost_item.id()]
cost_item_data["id"] = cost_item.id()
cost_item_data["name"] = cost_item.Name
cost_item_data["is_nested_by"] = []
cost_item_data["is_sum"] = cls.is_cost_item_sum(cost_item)
data.append(cost_item_data)
else:
return None
for rel in cost_item.IsNestedBy or []:
for sub_cost_item in rel.RelatedObjects or []:
cls.create_cost_item_json(sub_cost_item, cost_items,cost_item_data["is_nested_by"])
@classmethod
def is_cost_item_sum(cls, cost_item: ifcopenshell.entity_instance) -> bool:
cost_values = []
if cost_item.is_a("IfcCostItem"):
cost_values = cost_item.CostValues
elif cost_item.is_a("IfcCostValue"):
cost_values = cost_item.Components
for cost_value in cost_values or []:
if cost_value.Category == "*":
return True
return False
@classmethod
def currency(cls):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
+4
View File
@@ -49,6 +49,8 @@ class Covering(bonsai.core.tool.Covering):
def covering_poll_wall_selected( def covering_poll_wall_selected(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool: ) -> bool:
if not tool.Ifc.get():
return False
if not context.selected_objects or not context.active_object: if not context.selected_objects or not context.active_object:
operator.poll_message_set("No objects selected.") operator.poll_message_set("No objects selected.")
return False return False
@@ -62,6 +64,8 @@ class Covering(bonsai.core.tool.Covering):
def covering_poll_relating_type_check( def covering_poll_relating_type_check(
cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str
) -> bool: ) -> bool:
if not tool.Ifc.get():
return False
props = context.scene.BIMModelProperties props = context.scene.BIMModelProperties
relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id") relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id")
if relating_type_id is not None: if relating_type_id is not None:
+16 -7
View File
@@ -19,31 +19,40 @@
import os import os
import bpy import bpy
import ifcopenshell.express import ifcopenshell.express
import ifcopenshell.express.schema
import ifcopenshell.express.schema_class
import ifcopenshell.util.element
import bonsai.core.tool import bonsai.core.tool
import bonsai.tool as tool import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from mathutils import Vector
class Debug(bonsai.core.tool.Debug): class Debug(bonsai.core.tool.Debug):
@classmethod @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) IfcStore.schema_identifiers.append(schema.schema_name)
@classmethod @classmethod
def load_express(cls, filename): def load_express(cls, filename: str) -> ifcopenshell.express.schema_class.SchemaClass:
schema = ifcopenshell.express.parse(filename) schema = ifcopenshell.express.parse(filename)
ifcopenshell.register_schema(schema) ifcopenshell.register_schema(schema)
return schema return schema
@classmethod @classmethod
def purge_hdf5_cache(cls): def purge_hdf5_cache(cls) -> None:
cache_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache") 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")] filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")]
for f in filelist: 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 @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 = bpy.data.meshes.new("Debug")
mesh.from_pydata(verts, edges, []) mesh.from_pydata(verts, edges, [])
obj = bpy.data.objects.new(name, mesh) obj = bpy.data.objects.new(name, mesh)
@@ -51,13 +60,13 @@ class Debug(bonsai.core.tool.Debug):
return obj return obj
@classmethod @classmethod
def remove_unused_elements(cls, elements): def remove_unused_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
for element in elements: for element in elements:
ifcopenshell.util.element.remove_deep2(ifc_file, element) ifcopenshell.util.element.remove_deep2(ifc_file, element)
@classmethod @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() ifc_file = tool.Ifc.get()
# get list of ifc classes used in model # get list of ifc classes used in model
+11 -12
View File
@@ -1669,20 +1669,17 @@ class Drawing(bonsai.core.tool.Drawing):
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
updated_set = set() updated_set = set()
for i in elements: for i in elements:
# exclude annotations to avoid including annotations from other drawings # exclude annotations to avoid including annotations from other drawings
if not i.is_a("IfcAnnotation"): if not i.is_a("IfcAnnotation"):
updated_set.add(i) updated_set.add(i)
# add aggregate too, if element is host by one # add aggregate too, if element is host by one
if i.Decomposes: if decomposes := i.Decomposes:
aggregate = i.Decomposes[0].RelatingObject aggregate = decomposes[0].RelatingObject
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615 # 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) updated_set.add(aggregate)
elements = updated_set
# After the iteration is complete, update elements with updated set
elements.update(updated_set)
# add annotations from the current drawing # add annotations from the current drawing
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
@@ -1782,6 +1779,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod @classmethod
def activate_drawing(cls, camera: bpy.types.Object) -> None: def activate_drawing(cls, camera: bpy.types.Object) -> None:
selected_objects_before = bpy.context.selected_objects 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 # Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera) drawing = tool.Ifc.get_entity(camera)
@@ -1862,11 +1860,12 @@ class Drawing(bonsai.core.tool.Drawing):
element_obj_names.add(obj.name) element_obj_names.add(obj.name)
# Note that render visibility is only set on drawing generation time for speed. # Note that render visibility is only set on drawing generation time for speed.
[ for obj in bpy.context.view_layer.objects:
obj.hide_set(False) # Show the object if obj.name in element_obj_names:
for obj in bpy.context.view_layer.objects obj.hide_set(False) # Show the object
if obj.name in element_obj_names or not tool.Ifc.get_entity(obj) continue
] if (hide := non_ifc_objects_hide.get(obj)) is not None:
obj.hide_set(hide)
cls.import_camera_props(drawing, camera.data) cls.import_camera_props(drawing, camera.data)
+91 -43
View File
@@ -41,7 +41,7 @@ class Raycast(bonsai.core.tool.Raycast):
return all_objs return all_objs
@classmethod @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() obj_matrix = obj.matrix_world.copy()
bbox = [obj_matrix @ Vector(v) for v in obj.bound_box] 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) return (obj, bbox_2d)
@classmethod @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 x, y = mouse_pos
xmin, xmax, ymin, ymax = bbox 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: if xmin < x < xmax and ymin < y < ymax:
return True return True
else: else:
return False return False
@classmethod @classmethod
def get_viewport_ray_data(cls, context, event): def get_viewport_ray_data(cls, context, event, mouse_pos=None):
region = context.region region = context.region
rv3d = context.region_data 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) 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) 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 return ray_origin, ray_target, ray_direction
@classmethod @classmethod
def get_object_ray_data(cls, context, event, obj_matrix): def get_object_ray_data(cls, context, event, obj_matrix, mouse_pos=None):
ray_origin, ray_target, _ = cls.get_viewport_ray_data(context, event) 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() matrix_inv = obj_matrix.inverted()
ray_origin_obj = matrix_inv @ ray_origin ray_origin_obj = matrix_inv @ ray_origin
ray_target_obj = matrix_inv @ ray_target 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 return ray_origin_obj, ray_target_obj, ray_direction_obj
@classmethod @classmethod
def obj_ray_cast(cls, context, event, obj): def obj_ray_cast(cls, context, event, obj, mouse_pos=None):
ray_origin_obj, _, ray_direction_obj = cls.get_object_ray_data(context, event, obj.matrix_world.copy()) 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) success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success: if success:
return location, normal, face_index return location, normal, face_index
@@ -107,7 +123,65 @@ class Raycast(bonsai.core.tool.Raycast):
return None, None, None return None, None, None
@classmethod @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 region = context.region
rv3d = context.region_data rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y mouse_pos = event.mouse_region_x, event.mouse_region_y
@@ -118,43 +192,17 @@ class Raycast(bonsai.core.tool.Raycast):
except: except:
loc = Vector((0, 0, 0)) loc = Vector((0, 0, 0))
bm = bmesh.new() polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
if mesh is None: polyline_points = []
bm.from_mesh(obj.data) for point_data in polyline_data:
else: point = Vector((point_data.x, point_data.y, point_data.z))
verts = [bm.verts.new(obj.data.vertices[i].co) for i in mesh.vertices]
bm.faces.new(verts)
for edge in bm.edges: intersection, _ = mathutils.geometry.intersect_point_line(point, ray_target, loc)
v1 = edge.verts[0].co distance = (point - intersection).length
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
if distance < 0.2: if distance < 0.2:
return division_point, "Edge Center" polyline_points.append((point, "Vertex"))
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
return polyline_points
@classmethod @classmethod
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal): def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
+12 -7
View File
@@ -170,16 +170,21 @@ class Root(bonsai.core.tool.Root):
@classmethod @classmethod
def is_containable(cls, element: ifcopenshell.entity_instance) -> bool: 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 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 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 @classmethod
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool: def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
return element.is_a(ifc_class) return element.is_a(ifc_class)
+3 -2
View File
@@ -23,14 +23,15 @@ class Search(bonsai.core.tool.Search):
elif module == "diff": elif module == "diff":
return bpy.context.scene.DiffProperties.filter_groups return bpy.context.scene.DiffProperties.filter_groups
elif module == "drawing_include": 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": 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"): elif module.startswith("clash"):
_, clash_set_index, ab, clash_source_index = module.split("_") _, clash_set_index, ab, clash_source_index = module.split("_")
return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[
int(clash_source_index) int(clash_source_index)
].filter_groups ].filter_groups
assert False, f"Unsupported module: {module}"
@classmethod @classmethod
def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None: def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None:
+262 -117
View File
@@ -30,7 +30,8 @@ class Snap(bonsai.core.tool.Snap):
mouse_pos = None mouse_pos = None
snap_angle = None snap_angle = None
use_default_container = False use_default_container = False
snap_plane_method = "No Plane" snap_plane_method = None
snap_axis_method = None
@classmethod @classmethod
def set_use_default_container(cls, value=True): 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): def set_snap_plane_method(cls, value=True):
cls.snap_plane_method = value 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 @classmethod
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
matrix = obj.matrix_world.copy() matrix = obj.matrix_world.copy()
@@ -89,19 +104,24 @@ class Snap(bonsai.core.tool.Snap):
return snap_point return snap_point
@classmethod @classmethod
def update_snaping_point(cls, snap_point, snap_type): def update_snapping_point(cls, snap_point, snap_type):
try: try:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0]
except: except:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point.add() 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.x = snap_point[0]
snap_vertex.y = snap_point[1] snap_vertex.y = snap_point[1]
snap_vertex.z = snap_point[2] snap_vertex.z = snap_point[2]
snap_vertex.snap_type = snap_type snap_vertex.snap_type = snap_type
@classmethod @classmethod
def update_snaping_ref(cls, snap_point, snap_type): def update_snapping_ref(cls, snap_point, snap_type):
try: try:
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_ref[0] snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_ref[0]
except: except:
@@ -113,7 +133,7 @@ class Snap(bonsai.core.tool.Snap):
snap_vertex.snap_type = snap_type snap_vertex.snap_type = snap_type
@classmethod @classmethod
def clear_snaping_ref(cls): def clear_snapping_ref(cls):
bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear() bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear()
@classmethod @classmethod
@@ -140,7 +160,7 @@ class Snap(bonsai.core.tool.Snap):
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
if polyline_data: if polyline_data:
last_point = polyline_data[len(polyline_data) - 1] 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 return
polyline_point = bpy.context.scene.BIMModelProperties.polyline_point.add() 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): def create_axis_line_data(rot_mat, origin):
length = 1000 length = 1000
direction = Vector((1, 0, 0)) 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)) direction = Vector((0, 0, 1))
rot_dir = rot_mat.inverted() @ direction rot_dir = rot_mat.inverted() @ direction
start = origin + rot_dir * length start = origin + rot_dir * length
@@ -233,8 +253,6 @@ class Snap(bonsai.core.tool.Snap):
if cls.snap_plane_method == "YZ": if cls.snap_plane_method == "YZ":
pivot_axis = "X" pivot_axis = "X"
rectangle_data = create_axis_rectangle_data(last_point)
PolylineDecorator.set_axis_rectangle(rectangle_data)
for axis in snap_axis: for axis in snap_axis:
rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis)
start, end = create_axis_line_data(rot_mat, last_point) 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 return None, None, None, None
@classmethod @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 # Creates a mixed snap point between the locked axis and
# the object snap # the object snap
# TODO Use ALT key to give the user the option to choose between the two results. intersections = []
# TODO Create decorator for this intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((1, 0, 0))))
snap_point_vector = Vector((snap_point[0].x, snap_point[0].y, snap_point[0].z)) intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 1, 0))))
snap_point_axis_1 = ( intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 0, 1))))
Vector((snap_point[0].x + 1000, snap_point[0].y, elevation)), sorted_intersections = sorted(i for i in intersections if i is not None)
Vector((snap_point[0].x - 1000, snap_point[0].y, elevation)), if sorted_intersections[0]:
) return sorted_intersections[0], "Mix"
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])
@classmethod @classmethod
def snaping_movement(cls, context, event, objs_2d_bbox): def detect_snapping_points(cls, context, event, objs_2d_bbox):
region = context.region region = context.region
rv3d = context.region_data rv3d = context.region_data
space = context.space_data
cls.mouse_pos = event.mouse_region_x, event.mouse_region_y cls.mouse_pos = event.mouse_region_x, event.mouse_region_y
detected_snaps = []
offset = 15
snap_threshold = 0.3 snap_threshold = 0.3
offset = 10
mouse_offset = ( mouse_offset = (
(-offset, offset), (-offset, offset),
(0, offset), (0, offset),
@@ -316,13 +314,15 @@ class Snap(bonsai.core.tool.Snap):
def select_plane_method(): def select_plane_method():
if not last_polyline_point: if not last_polyline_point:
plane_origin = Vector((0, 0, 0)) 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 camera_rotation = rv3d.view_rotation
plane_origin = Vector((0, 0, 0)) 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: if cls.use_default_container:
plane_origin = Vector((0, 0, elevation)) plane_origin = Vector((0, 0, elevation))
elif not last_polyline_point: 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_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 0, 1)) 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: if last_polyline_point:
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((0, 1, 0)) plane_normal = Vector((0, 1, 0))
@@ -356,7 +356,7 @@ class Snap(bonsai.core.tool.Snap):
original_mouse_pos = cls.mouse_pos original_mouse_pos = cls.mouse_pos
for value in mouse_offset: for value in mouse_offset:
cls.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) 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: if hit:
break break
cls.mouse_pos = original_mouse_pos cls.mouse_pos = original_mouse_pos
@@ -370,18 +370,49 @@ class Snap(bonsai.core.tool.Snap):
best_hit = hit_world best_hit = hit_world
best_face_index = face_index best_face_index = face_index
if best_obj is not None: if best_obj is not None:
return best_obj, best_hit, best_face_index return best_obj, best_hit, best_face_index
else: else:
return None, None, None 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: try:
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
last_polyline_point = polyline_data[len(polyline_data) - 1] last_polyline_point = polyline_data[len(polyline_data) - 1]
except: except:
last_polyline_point = None 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 elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin, plane_normal = select_plane_method() plane_origin, plane_normal = select_plane_method()
@@ -390,75 +421,139 @@ class Snap(bonsai.core.tool.Snap):
axis_start = None axis_start = None
axis_end = None axis_end = None
if cls.snap_plane_method in {"XY", "XZ", "YZ"}:
# Locks snap into an angle axis # TODO It only work for XY plane. Make it work also for None plane_method
if event.shift: 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) rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
else: else:
cls.snap_angle = None rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, 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
if rot_intersection: if rot_intersection:
cls.update_snaping_point(rot_intersection, "Axis") detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)})
else:
cls.update_snaping_point(intersection, "Plane") detected_snaps.append({"Plane": intersection})
return detected_snaps
@classmethod @classmethod
def validate_input(cls, input_number): def select_snapping_points(cls, context, event, detected_snaps):
grammar = """ snapping_points = []
start: dim expr? for origin in detected_snaps:
dim: NUMBER if "Object" in list(origin.keys()):
expr: (ADD | SUB | MUL | DIV) NUMBER 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+)?/ NUMBER: /-?\\d+(?:\\.\\d+)?/
ADD: "+" ADD: "+"
@@ -470,8 +565,34 @@ class Snap(bonsai.core.tool.Snap):
""" """
class InputTransform(Transformer): 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): def dim(self, args):
return float(args[0]) return args[0]
def expr(self, args): def expr(self, args):
op = args[0] op = args[0]
@@ -485,17 +606,41 @@ class Snap(bonsai.core.tool.Snap):
elif op == "/": elif op == "/":
return lambda x: x / value return lambda x: x / value
def FORMULA(cls, args):
return args[0]
def start(self, args): def start(self, args):
dimension = args[0] i = 0
if len(args) > 1: if args[0] == "=":
expression = args[1] i += 1
return expression(dimension)
else: 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: try:
parser = Lark(grammar, parser="lalr", transformer=InputTransform()) if bpy.context.scene.unit_settings.system == "IMPERIAL":
result = parser.parse(input_number) 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) return True, str(result)
except: except:
return False, "0" return False, "0"
+63 -15
View File
@@ -39,7 +39,8 @@ import json
from math import pi from math import pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from shapely import Polygon 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): 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 target_obj.matrix_world = relative_to_obj.matrix_world @ matrix
@classmethod @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") bpy.ops.object.select_all(action="DESELECT")
for product in products: for product in products:
obj = tool.Ifc.get_object(product) 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) container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = {} results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children: if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else: else:
@@ -218,25 +219,48 @@ class Spatial(bonsai.core.tool.Spatial):
ifc_class = element.is_a() ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0 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()}" 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}) class_data = results.setdefault(ifc_class, {})
results[ifc_class][ifc_definition_id]["total"] += 1 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 total_elements = 0
for ifc_class in sorted(results.keys()): for ifc_class in sorted(results.keys()):
new = props.elements.add() new = props.elements.add()
new.name = ifc_class 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 total = 0
for ifc_definition_id in sorted( for ifc_definition_id in sorted(
results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"] results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"]
): ):
new2 = props.elements.add() type_data = results[ifc_class][ifc_definition_id]
new2.is_type = True total2 = len(type_data["elements"])
new2.name = results[ifc_class][ifc_definition_id]["type_name"] if class_is_expanded:
new2.ifc_class = ifc_class new2 = props.elements.add()
new2.total = results[ifc_class][ifc_definition_id]["total"] new2.type = "TYPE"
new2.ifc_definition_id = ifc_definition_id new2.name = type_data["type_name"]
total += new2.total 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 new.total = total
total_elements += total total_elements += total
@@ -248,11 +272,12 @@ class Spatial(bonsai.core.tool.Spatial):
elements: List[ifcopenshell.entity_instance], elements: List[ifcopenshell.entity_instance],
ifc_class: str | None, ifc_class: str | None,
relating_type: ifcopenshell.entity_instance | None, relating_type: ifcopenshell.entity_instance | None,
is_untyped: bool | None, is_untyped: bool,
keyword: str | None, keyword: str | None,
) -> filter[ifcopenshell.entity_instance]: ) -> filter[ifcopenshell.entity_instance]:
keyword = keyword.lower() if keyword else keyword keyword = keyword.lower() if keyword else keyword
def filter_element(element):
def filter_element(element: ifcopenshell.entity_instance) -> bool:
if ifc_class: if ifc_class:
if not element.is_a(ifc_class): if not element.is_a(ifc_class):
return False return False
@@ -268,6 +293,7 @@ class Spatial(bonsai.core.tool.Spatial):
if keyword not in f"{element.is_a()} {type_name}".lower(): if keyword not in f"{element.is_a()} {type_name}".lower():
return False return False
return True return True
return filter(filter_element, elements) return filter(filter_element, elements)
@classmethod @classmethod
@@ -336,6 +362,28 @@ class Spatial(bonsai.core.tool.Spatial):
contracted_containers.remove(container.id()) contracted_containers.remove(container.id())
props.contracted_containers = json.dumps(contracted_containers) 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 # HERE STARTS SPATIAL TOOL
@classmethod @classmethod
+69 -21
View File
@@ -36,6 +36,8 @@ import queue
import json import json
from time import sleep from time import sleep
from pathlib import Path from pathlib import Path
import bonsai.core.sequence
import bonsai.core.cost
sio = None sio = None
ws_process = None ws_process = None
@@ -310,6 +312,8 @@ class Web(bonsai.core.tool.Web):
elif operator["sourcePage"] == "demo": elif operator["sourcePage"] == "demo":
message = operator["operator"]["message"] message = operator["operator"]["message"]
print(f"Message from demo page: {message}") print(f"Message from demo page: {message}")
elif operator["sourcePage"] == "cost":
cls.handle_cost_operator(operator["operator"])
return 1.0 return 1.0
@classmethod @classmethod
@@ -328,6 +332,48 @@ class Web(bonsai.core.tool.Web):
bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active = obj
obj.select_set(True) 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 @classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None: 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. operator_data (dict): A dictionary containing the operator data.
""" """
ifc_file = tool.Ifc.get() 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": if operator_data["type"] == "editTask":
task_id = int(operator_data["taskId"]) task_id = int(operator_data["taskId"])
task = ifc_file.by_id(task_id) task = ifc_file.by_id(task_id)
task_time = task.TaskTime task_time = task.TaskTime
column = operator_data["column"] column = operator_data["column"]
new_value = operator_data["value"] 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: bpy.ops.bim.load_task_properties()
ifcopenshell.api.sequence.edit_task( # after updating, send new gantt data to handle the case where
ifc_file, task, attributes={IFC_TASK_ATTRIBUTE_MAP[column]: str(new_value)} # changing a task cascades and changes other tasks. as this wouldn't
) # be reflected in the web ui
except AttributeError: work_schedule = ifc_file.by_id(operator_data["workScheduleId"])
if task_time is None: task_json = tool.Sequence.create_tasks_json(work_schedule)
ifcopenshell.api.sequence.add_task_time(ifc_file, task) gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
task_time = task.TaskTime cls.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
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")
@classmethod @classmethod
def handle_drawings_operator(cls, operator_data: dict) -> None: def handle_drawings_operator(cls, operator_data: dict) -> None:
+8
View File
@@ -83,6 +83,14 @@ img.icon {
font-size: small; font-size: small;
color: #808080; 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 There are always solutions to all of these, but an understanding of the type of
size limitation you are up against will always help. 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 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 Using Blender 3.3 and above will result in a faster load time (~50%) compared to
older Blender versions. 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 Processing models headlessly
---------------------------- ----------------------------
+6 -9
View File
@@ -52,15 +52,11 @@ and data-rich OpenBIM with Blender :)
:caption: Guides :caption: Guides
:maxdepth: 2 :maxdepth: 2
guides/viewing/index guides/viewing/dealing_with_large_models
guides/authoring/index guides/authoring/georeferencing
guides/drawings/index guides/authoring/git_support
guides/structural_analysis/index
guides/services/index
guides/costing_and_scheduling/index
guides/facility_management/index
guides/coordination/index
guides/development/index guides/development/index
guides/authoring/other_addons
guides/troubleshooting guides/troubleshooting
.. toctree:: .. toctree::
@@ -68,7 +64,8 @@ and data-rich OpenBIM with Blender :)
:caption: Reference :caption: Reference
:maxdepth: 2 :maxdepth: 2
reference/workspace reference/interface
reference/topbar
reference/properties reference/properties
Need more help? Join the `live chat <https://osarch.org/chat/>`__ or `community Need more help? Join the `live chat <https://osarch.org/chat/>`__ or `community
+21 -19
View File
@@ -16,21 +16,21 @@ you will need to categorise your 3D elements (such as "Wall", "Column",
Creating a single object Creating a single object
------------------------ ------------------------
In the **Properties** panel on the right, ensure the icon for the **Scene Go to :menuselection:`Topbar --> File --> New IFC Project`, and select **New
Properties** tab is active showing the **Project Overview**. Click on **Create Metric (m) Project**. This will begin a blank IFC4 project.
Project** to create a blank IFC project.
.. image:: images/create-project.png .. image:: images/create-project.png
In the left **Outliner** panel, you will see a hierarchy of spaces that has In the :menuselection:`Properties --> Project Overview --> Spatial
been automatically created for you. This hierarchy is known is the **Spatial Decomposition` panel, you will see a hierarchy of spaces that has been
Tree**. automatically created for you. This hierarchy is known is the **Spatial Tree**.
.. image:: images/default-spatial-tree.png .. image:: images/default-spatial-tree.png
There are many ways to create objects. In practice, you should use an element 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 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 .. image:: images/add-cube.png
@@ -45,10 +45,10 @@ Class**.
Class**! Class**!
Select the cube (selected objects are highlighted in orange, careful not to Select the cube (selected objects are highlighted in orange, careful not to
select anything else!) and switch to the **Object Information** tab. Let's select anything else!) and switch to the :menuselection:`Properties --> Object
pretend our Cube is a column, so select **IfcElement** from the **Products** Information` tab. Let's pretend our Cube is a column, so select **IfcElement**
dropdown, **IfcColumn** from the **Class** drop-down, and press **Assign IFC from the **Products** dropdown, **IfcColumn** from the **Class** drop-down, and
Class**. press **Assign IFC Class**.
.. image:: images/assign-class.png .. 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! 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 All IFC objects must also belong inside the **Spatial Tree**. In the
**Outliner** panel, you will see that your newly created **IfcColumn/Cube** has :menuselection:`Properties --> Object Information --> Spatial Container` panel,
been automatically placed in **IfcBuildingStorey/My Storey**. 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 .. image:: images/save-project.png
Congratulations! You have now created your first OpenBIM model from Blender! You Congratulations! You have now created your first IFC model with Bonsai! You can
can open the IFC file in any other program, and you will see something similar open the IFC file in any other program, and you will see something similar to
to the image below. Three simple open source online viewers you can test with the image below. Three simple open source online viewers you can test with are
are `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor `IfcPipeline <https://view.ifcopenshell.org>`__, `ThatOpenEditor
<https://platform.thatopen.com/app>`__, and `3DViewer <https://platform.thatopen.com/app>`__, and `3DViewer
<https://3dviewer.net/>`__. <https://3dviewer.net/>`__.
+41 -37
View File
@@ -21,31 +21,33 @@ use some creativity when reading the data :)
Loading a model Loading a model
--------------- ---------------
Blender's interface is divided into three panels. The left **Outliner** panel Bonsai's interface is divided into three panels. The left
shows a tree of geometric objects. The centre main **Viewport** panel shows 3D :menuselection:`Outliner` panel shows geometric objects. The centre main
geometry. The right **Properties** panel shows data and relationships. :menuselection:`3D Viewport` panel shows 3D geometry. The right
:menuselection:`Properties` panel shows data and relationships.
.. image:: images/bonsai-layout.png .. image:: images/bonsai-layout.png
The **Properties** panel has tabs to switch between different types of The :menuselection:`Properties` panel has tabs to switch between different
properties. types of properties.
.. image:: images/properties-tabs.png .. 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 .. 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 .. image:: images/example-project.png
Take a look at the **Project Info** subpanel. It shows the loaded filename, as Take a look at the :menuselection:`Properties --> Project Overview --> Project
well as the **IFC Schema**. There are two commonly seen **IFC Schema** Info` panel. It shows the loaded filename, as well as the **IFC Schema**. There
versions: IFC2X3 and IFC4. Checking the **IFC Schema** is important because it are two commonly seen **IFC Schema** versions: IFC2X3 and IFC4. Checking the
has an impact on what BIM data may be stored. IFC4 is the newer version and it **IFC Schema** is important because it has an impact on what BIM data may be
is recommended to use IFC4 models as it has significantly more BIM capabilities stored. IFC4 is the newer version and it is recommended to use IFC4 models as
compared to IFC2X3. it has significantly more BIM capabilities compared to IFC2X3.
.. tip:: .. tip::
@@ -57,10 +59,10 @@ Navigating a model in 3D
------------------------ ------------------------
To navigate, can use the **Navigate Gizmo** on the top right corner of the 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 :menuselection:`3D Viewport`. Click and drag on the coloured axes to **Orbit**,
drag on the magnifying glass to **Zoom**, and click and drag on the hand icon to click and drag on the magnifying glass to **Zoom**, and click and drag on the
**Pan**. You can also click on the grid icon to switch between perspective and hand icon to **Pan**. You can also click on the grid icon to switch between
orthographic view. perspective and orthographic view.
To switch to a top view, front view, or side view, click the relevant axis on To switch to a top view, front view, or side view, click the relevant axis on
the **Navigate Gizmo**. the **Navigate Gizmo**.
@@ -68,42 +70,44 @@ the **Navigate Gizmo**.
.. image:: images/navigate-gizmo.png .. image:: images/navigate-gizmo.png
You can also use your mouse to navigate. Hover your mouse over the **Viewport** 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 panel and click and drag the Middle Mouse Button (:kbd:`MMB`) to **Orbit**.
the mousewheel to **Zoom**, and use ``Shift-MMB`` to **Pan**. 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, 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 front, or side view. Use :kbd:`7` for top view, :kbd:`1` for front view, and
side view. :kbd:`3` for side view.
.. warning:: .. warning::
Blender's hotkeys are context sensitive. This means that a hotkey has a 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. 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 If you press :kbd:`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 :menuselection:`3D Viewport`. Be very careful where your mouse is, or you
hotkey and it will have unintended consequences! 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 If you click on an object, such as a wall in the :menuselection:`3D Viewport`,
zoom to the selected object by clicking on ``View > Frame Selected``. The hotkey you can zoom to the selected object by clicking on :menuselection:`3D Viewport
is the ``.`` button on the numpad. After zooming into an element, when you --> View` then **Frame Selected**. The hotkey is the :kbd:`.` button on the
**Orbit** the 3D view will rotate around the center of that element. 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 You can also zoom to all objects in the project by clicking on
All``. :menuselection:`3D Viewport --> View` then **Frame All**.
.. image:: images/frame-selected.png .. image:: images/frame-selected.png
Another good way to navigate is by flying or walking around similar to a video 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-``` game. Click on :menuselection:`3D Viewport --> Explore Tool`, then you can
hotkey (the backtick key is usually to the left of the number 1 on the press :kbd:`Shift-W` to activate **Walk Mode**. Use the :kbd:`WASD` keys and
keyboard). With **Walk Navigation** enabled, use the ``WASD`` keys and the mouse the mouse to move around like a video game. You can use the :kbd:`Shift` key to
to move around like a video game. You can use the ``Shift`` key to switch switch between moving fast and slow. If you scroll with the mousewheel, it will
between moving fast and slow. If you scroll with the mousewheel, it will adjust adjust the speed that you move at.
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 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 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 .. image:: images/x-ray-mode.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

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