Compare commits

...

43 Commits

Author SHA1 Message Date
Dion Moult e072870fd3 Fix bcf pypi build 2024-09-01 13:35:57 +10:00
Dion Moult af4475fe46 Run black 2024-09-01 13:26:58 +10:00
Dion Moult eb3104367a Bump version to v0.8.0 2024-09-01 13:23:54 +10:00
Dion Moult 90f9ebd25e Add dev install instructins to official docs 2024-09-01 13:23:31 +10:00
Yassine Oualid f2554bc968 - Add button to generate cost schedule
- Feature to view, edit and add cost item values. ( Right Click - Edit ).
2024-09-01 01:40:53 +01:00
Dion Moult 53a78f38e5 You can now assign objects to the selected container in the spatial decomposition panel 2024-09-01 08:13:29 +10:00
Dion Moult b476cb825c Fix #5265. Accommodate invalid material sets. 2024-09-01 07:53:49 +10:00
Bruno Perdigão ec21574be5 Fix measure tool to now show the angle for the first point. 2024-08-31 14:39:58 -03:00
Bruno Perdigão b552665dd9 Round angle in polytool input numbers calculations. 2024-08-31 14:39:58 -03:00
Bruno Perdigão 48b630696f Fix 3ceeaa7 2024-08-31 14:39:58 -03:00
Bruno Perdigão 8c94ef6e54 A better fix for dabb85c. 2024-08-31 14:39:58 -03:00
Bruno Perdigão bdcadfd100 Refactor tool.Cad.angle_3_vectors
This functions was built to work with the input calculations for the polytool.
Part of its code was being duplicated in another part of the code. The refactor
keeps most of the calculation in the same place.
2024-08-31 14:39:58 -03:00
Bruno Perdigão 90e83d77a7 more improvement on distance and angle calculations 2024-08-31 14:39:58 -03:00
Bruno Perdigão 2e80363bac improved general input calculations 2024-08-31 14:39:58 -03:00
Bruno Perdigão b6f80eabb4 Fix input calculation and conversion for measure tool 2024-08-31 14:39:58 -03:00
Bruno Perdigão c93e52089b Fix input calculation and conversion for wall polytool 2024-08-31 14:39:58 -03:00
Andrej730 76ba3947f5 Expose header properties with invalid values #5246 2024-08-31 14:31:41 +02:00
Dion Moult 50cb09aac2 See #3880. Freestyle support for perspective vector linework. 2024-08-31 20:56:10 +10:00
Andrej730 dac1aaa7f3 Errors adding roof, stair, railing types #5261
It wasn't considering that type object can be hidden and can't be active.
Removed obj property since it can be covered with just temp_override.
2024-08-31 13:30:12 +05:00
Dion Moult eed603bbcd Bump IOS 2024-08-31 18:09:54 +10:00
Dion Moult 1d62f6f736 Support experimental freestyle linework generation 2024-08-31 18:09:14 +10:00
Andrej730 799cf47316 Fix error removing a cost schedule (after 7a44f9e) #5264
It wasn't really leaving orphaned data and interrupting the process of the ifc object deletion but it was leading to confusing errors
2024-08-31 13:00:11 +05:00
Dion Moult 03935a91da Fix bug where linework merging may create polygons where there should be interior holes instead. 2024-08-30 23:53:02 +10:00
Dion Moult 37baa9235a Fix bug where a voided element by OCC will be considered non-manifold. Remove doubles with a very small distance fixes it. 2024-08-30 23:51:47 +10:00
Dion Moult b4b2b60935 New bisect-based drawing cut mode. This replaces OCC's cut (but keeps the projection HLR) with a blender bisect based cut 2024-08-30 20:33:09 +10:00
Dion Moult a11c7535d8 Turn fill modes (shapely / svgfill) into a dropdown to prevent selecting both and crashing 2024-08-30 20:33:09 +10:00
Andrej730 e8d6dd333c Fix failing import if 'sun position' extension (9633d92)
ping @chiragsingh1711
2024-08-30 12:15:00 +05:00
Chirag Singh 9633d92c17 Light/radiance (#5252)
* Cam fix, Sun Fix, import-export as json

* Ran black formatting
2024-08-30 08:32:45 +10:00
Andrej730 afd5a5b23e Fix get_info_2 #5248 after d80bcd1 2024-08-29 18:57:47 +05:00
Dion Moult 15481c0a59 Bump docs to use spatial panel. Format menuselection. 2024-08-29 23:30:53 +10:00
Dion Moult 32b37b8823 Implement recursive toggling for elements in spatial panel 2024-08-29 23:30:53 +10:00
Andrej730 f3ab214752 Use first layer/profile from the set to pick up the material #5238
As currently we do not support multilayer/profile geometry and therefore we can't correctly represent them when they have multiple styles. As a workaround we'll use the material/style from the first layer and apply it to the entire object.
2024-08-29 17:21:21 +05:00
Dion Moult 4a59b30f1b Fix #5244. Prioritise collection of drawing groups over containment groups 2024-08-29 18:42:42 +10:00
Andrej730 87d6960356 Fix #5249 after acf135a 2024-08-29 13:42:08 +05:00
Andrej730 aff893e690 bim.create_drawing - poll message 2024-08-29 13:42:08 +05:00
Dion Moult 486eb259ea Fix #5245. Consider situation where object may not be loaded due to partial loading. 2024-08-29 18:09:31 +10:00
Ziad-I 8164b496f8 fix #5219 underlay links are now base64 encoded in the svg before sending it to web UI
this is not really the best solution and might need to look for better ones
2024-08-29 10:50:50 +03:00
Dion Moult fda23f350a Fix #5247. Fix typo when opening drawing web UI. 2024-08-29 17:19:28 +10:00
Dion Moult 711c597065 See #5231. Workaround for missing offset. 2024-08-29 16:22:42 +10:00
Dion Moult 80a396dc12 You can now select geometry library in advanced settings 2024-08-29 16:22:42 +10:00
Dion Moult 2b70f12d44 More bonsai name fixes 2024-08-29 15:17:52 +10:00
Dion Moult c3bcc2d90c See #5243. types_with_super() is no longer available. Also maybe overkill in this scenario. 2024-08-29 10:11:01 +10:00
Dion Moult 88e5a51f01 Merge pull request #5236 from IfcOpenShell/webui_updates
Add RMB context menu to web UI spreadsheets and small updates
2024-08-29 09:55:55 +10:00
68 changed files with 1830 additions and 532 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v2 # https://github.com/actions/checkout
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
with:
python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
python-version: '3.11' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- run: echo ${{ env.DATE }}
- name: Get current date
+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.
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~~ Bonsai Add-on (an add-on to Blender providing a graphical IFC authoring platform),
to convert IFC models to other formats), Bonsai (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.
For more information, see:
@@ -37,7 +37,7 @@ Contents
| 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/) |
| ~~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/) |
| 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=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&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/) |
| 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/) |
+1 -1
View File
@@ -1 +1 @@
0.7.11
0.8.0
+1 -1
View File
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
OLD:=7e6607a
OLD:=03935a9
.PHONY: bump
bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
+44 -4
View File
@@ -1,6 +1,6 @@
import sys
import os
import webbrowser
bonsai_lib_path = os.environ.get("BONSAI_LIB_PATH")
bonsai_version = os.environ.get("BONSAI_VERSION")
@@ -13,6 +13,8 @@ from aiohttp import web
import socketio
import pystache
import json
import base64
import xml.etree.ElementTree as ET
sio_port = 8080 # default port
@@ -56,9 +58,8 @@ class WebNamespace(socketio.AsyncNamespace):
async def on_get_svg(self, sid, data):
file_path = data["path"]
with open(file_path, "r") as file:
svg_data = file.read()
await sio.emit("svg_data", svg_data, room=sid, namespace="/web")
svg = await self.process_svg(file_path)
await sio.emit("svg_data", svg, room=sid, namespace="/web")
async def send_cached_messages(self, sid):
# Send cached messages to the connected web client
@@ -70,6 +71,35 @@ class WebNamespace(socketio.AsyncNamespace):
if "demo_data" in messages:
await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid)
async def process_svg(self, file_path):
def encode_image(filepath):
# Encode file content to base64 string
with open(filepath, "rb") as file:
encoded_string = base64.b64encode(file.read()).decode("utf-8")
return f"data:image;base64,{encoded_string}"
file_dir = os.path.dirname(file_path)
ET.register_namespace("", "http://www.w3.org/2000/svg")
ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
tree = ET.parse(file_path)
root = tree.getroot()
namespaces = {
"svg": "http://www.w3.org/2000/svg",
"xlink": "http://www.w3.org/1999/xlink",
}
for element in root.findall(".//svg:image[@xlink:href]", namespaces):
href = element.get("{http://www.w3.org/1999/xlink}href")
if href and href.endswith((".png", ".svg", ".jpeg")):
img_path = os.path.join(file_dir, href)
if os.path.exists(img_path):
base64_data = encode_image(img_path)
element.set("{http://www.w3.org/1999/xlink}href", base64_data)
return ET.tostring(root, "unicode")
# Blender namespace
class BlenderNamespace(socketio.AsyncNamespace):
@@ -136,6 +166,16 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["cost_schedules"] = data
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_values(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_values"] = data
await sio.emit("cost_values", {"blenderId": sid, "data": data}, namespace="/web")
async def on_cost_value(self, sid, data):
print(f"Cost values from Blender client {sid}")
blender_messages[sid]["cost_value"] = data
await sio.emit("cost_value", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
@@ -0,0 +1,89 @@
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: rgb(51, 51, 51);
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
color: white;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
color: black;
}
/* Context menu button is clicked, should zoom into object and change color */
.context-menu button:active {
background-color: #ff9634;
color: rgba(37, 51, 77, 0.466);
/* zoom */
transform: scale(0.9);
}
#cost-items tr:hover {
background-color: #f0f0f0;
color : black;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: #363636;
padding: 10px;
}
.cost-values-form {
position: absolute; /* Set initial position to absolute */
background-color: white;
border: 1px solid black;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 9999;
cursor: move; /* Change cursor to move */
}
.cost-values-table {
width: 100%;
border-collapse: collapse;
}
.cost-values-table th, .cost-values-table td {
border: 1px solid #ddd;
padding: 8px;
}
.cost-values-table th {
background-color: #f2f2f2;
text-align: left;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}
@@ -1,4 +1,5 @@
@import url("./components/card.css");
@import url("./components/contextMenu.css");
:root {
--font-family: Arial, sans-serif;
@@ -23,6 +23,8 @@ function connectSocket() {
socket.on("connect", handleWebConnect);
socket.on("cost_schedules", handleCostSchedulesData);
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
}
function handleBlenderConnect(blenderId) {
@@ -46,8 +48,20 @@ function handleBlenderDisconnect(blenderId) {
});
}
function removeTableElement(blenderId) {
$("#cost-items-" + blenderId).remove();
}
function handleCostValueData(data) {
const costItemId = data.data["cost_value"]["cost_item_id"];
const costValueId = data.data["cost_value"]["cost_value_id"];
console.log("Handling cost value data", costItemId, costValueId);
CostUI.addNewCostValueRow(costItemId, costValueId);
}
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
@@ -56,6 +70,29 @@ function handleConnectedClients(data) {
});
}
function handleCostValuesData(data) {
CostUI.createCostValuesForm({
costValues: data.data["cost_values"]["cost_values"],
costItemId: data.data["cost_values"]["cost_item_id"],
callbacks: {
'editCostValues': editCostValues,
'addCostValue': addCostValue,
},
});
}
function addCostValue(costItemId) {
executeOperator({ type: "addCostValue", costItemId: costItemId });
}
function editCostValues(costItemId, costValues) {
executeOperator({
type: "editCostValues",
costItemId: costItemId,
costValues: costValues
});
}
function handleThemeData(themeData) {
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -99,7 +136,6 @@ function setTheme(theme) {
}
function addCostItem(costItemId) {
console.log("addCostItem", costItemId);
executeOperator({ type: "addCostItem", costItemId: costItemId });
}
@@ -119,11 +155,7 @@ function handleCostSchedulesData(data) {
const blenderId = data.blenderId;
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
const currency = data.data["cost_schedules"]["currency"]["name"];
console.log(data.data["cost_schedules"]);
const costScheduleDiv = $("#cost-schedules");
costSchedules.forEach((costSchedule) => {
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
@@ -136,7 +168,6 @@ function handleCostSchedulesData(data) {
}
function handleCostItemsData(data) {
console.log(data);
CostUI.createCostSchedule({
data: data.data["cost_items"],
blenderID: data.blenderId,
@@ -144,6 +175,7 @@ function handleCostItemsData(data) {
"addCostItem": addCostItem,
"selectAssignedElements": selectAssignedElements,
'editCostItemName': editCostItemName,
'enableEditingCostValues': enableEditingCostValues,
}
});
}
@@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) {
function getCostSchedules(blenderId) {
executeOperator({ type: "getCostSchedules" }, blenderId);
}
function enableEditingCostValues(costItemId) {
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
}
@@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -115,7 +114,6 @@ function handleThemeData(themeData) {
}
const cssRule = generateCssVariableRule(themeData.theme);
console.log(cssRule);
var styleElement = $("#gantt-stylesheet")[0];
if (styleElement) {
@@ -133,7 +131,6 @@ function handleWorkScheduleData(data) {
const workSchedules = data["data"]["work_schedule_info"];
workSchedules.forEach((workSchedule) => {
console.log(workSchedule);
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
const callback = () => loadWorkSchedule(workSchedule.id);
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
@@ -142,11 +139,8 @@ function handleWorkScheduleData(data) {
}
function handleGanttData(data) {
console.log("running handleGanttData");
const blenderId = data["blenderId"];
console.log(data);
const filename = data["data"]["ifc_file"];
const ganttTasks = data["data"]["gantt_data"]["tasks"];
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
@@ -183,7 +177,6 @@ function handleDefaultData(data) {
const blenderId = data["blenderId"];
const isDirty = data["data"]["is_dirty"];
showWarning(blenderId, isDirty);
console.log('default data',data);
}
// Function to add a new gantt with data and filename
@@ -1,14 +1,6 @@
export class CostUI {
constructor() {}
createButton() {
console.log("Button created");
}
createInput() {
console.log("Input created");
}
static isCostScheduleLoaded(id) {
const existingTable = document.getElementById('cost-items-' + id);
return existingTable !== null;
@@ -17,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
static createTable(id) {
static createTable(id, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
@@ -51,7 +43,7 @@ export class CostUI {
CostUI.addTableStyles(id);
// Create context menu
CostUI.createContextMenu();
CostUI.createContextMenu(callbacks);
table.get_blender_id = function() {
return this.getAttribute("id").split("-")[2];
@@ -71,52 +63,19 @@ export class CostUI {
#cost-items-${id} td:not(:nth-child(1)) {
width: 100px; /* Set a fixed width for other columns */
}
th {
position: relative;
}
.resizer {
position: absolute;
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.context-menu {
display: none;
position: absolute;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.context-menu button:hover {
background-color: #f0f0f0;
}
#cost-items tr:hover {
background-color: #f0f0f0; /* Change this color to your desired hover color */
}
`;
document.head.appendChild(style);
}
static createContextMenu() {
static createContextMenu(callbacks) {
// Create context menu
const contextMenu = document.createElement("div");
contextMenu.id = "context-menu";
contextMenu.classList.add("context-menu");
contextMenu.innerHTML = `
<button id="edit-button">Edit</button>
<button id="add-cost-item-button">Add sub-cost</button>
<button id="edit-cost-values-button">Edit</button>
<button id="delete-button">Delete</button>
<button id="duplicate-button">Duplicate</button>
`;
@@ -126,6 +85,7 @@ export class CostUI {
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetRow.parentElement.id === "cost-items") {
const contextMenu = document.getElementById("context-menu");
contextMenu.style.display = "block";
@@ -134,6 +94,7 @@ export class CostUI {
// Store the target row in the context menu for later use
contextMenu.targetRow = targetRow;
contextMenu.targetCell = targetCell;
} else {
document.getElementById("context-menu").style.display = "none";
}
@@ -146,14 +107,32 @@ export class CostUI {
}
});
document.getElementById("edit-button").addEventListener("click", function() {
const editCostValuesButton = document.getElementById("edit-cost-values-button");
editCostValuesButton.addEventListener("click", function() {
console.log("Edit cost values executed");
const targetRow = document.getElementById("context-menu").targetRow;
const targetCell = document.getElementById("context-menu").targetCell;
if (targetRow) {
// Implement your edit action here
console.log("Edit row:", targetRow.getAttribute("id"));
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.enableEditingCostValues ? callbacks.enableEditingCostValues(costItemId) : null;
}
});
const addButton = document.getElementById("add-cost-item-button");
if (!addButton.dataset.listenerAdded) {
function addCostItemHandler(e) {
e.stopPropagation();
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
callbacks.addCostItem ? callbacks.addCostItem(costItemId) : null;
}
document.getElementById("context-menu").style.display = "none";
}
addButton.addEventListener("click", addCostItemHandler);
addButton.dataset.listenerAdded = "true";
}
document.getElementById("delete-button").addEventListener("click", function() {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
@@ -246,7 +225,7 @@ export class CostUI {
}
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
const [table, tbody] = CostUI.createTable(blenderID);
const [table, tbody] = CostUI.createTable(blenderID, callbacks);
CostUI.createCostItem(data, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
@@ -263,6 +242,35 @@ export class CostUI {
}
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
const row = CostUI.createTableRow(obj, nestingLevel, parentID);
const expandButton = CostUI.createExpandButton(obj);
const nameCell = CostUI.createNameCell(obj, nestingLevel, expandButton, callbacks);
const totalCostQuantityCell = CostUI.createTableCell(obj.TotalCostQuantity);
const unitSymbolCell = CostUI.createTableCell(obj.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(obj.TotalAppliedValue);
const totalCostCell = CostUI.createTotalCostCell(obj);
const flexContainerCell = CostUI.createFlexContainerCell(obj, callbacks);
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
}
static createTableRow(obj, nestingLevel, parentID) {
const row = document.createElement("tr");
row.setAttribute("id", obj.id);
row.setAttribute("parent-id", parentID);
@@ -270,6 +278,10 @@ export class CostUI {
row.classList.add("nested");
row.classList.add(`level-${nestingLevel}`);
}
return row;
}
static createExpandButton(obj) {
const expandButton = document.createElement("button");
expandButton.classList.add("toggle-button");
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
@@ -277,51 +289,62 @@ export class CostUI {
} else {
expandButton.style.visibility = "hidden";
}
//row.appendChild(expandButton);
expandButton.addEventListener("click", function() {
CostUI.contractExpandRow.call(this, obj.id);
});
return expandButton;
}
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
const nameCell = document.createElement("td");
const nameInput = document.createElement("input");
nameInput.value = obj.name ? obj.name : "Unnamed";
nameInput.addEventListener("change", function() {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
});
nameInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
}
});
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
nameCell.appendChild(expandButton);
nameCell.appendChild(nameInput);
row.appendChild(nameCell);
const totalCostQuantityCell = document.createElement("td");
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
row.appendChild(totalCostQuantityCell);
const unitSymbolCell = document.createElement("td");
unitSymbolCell.textContent = obj.UnitSymbol;
row.appendChild(unitSymbolCell);
const totalAppliedValueCell = document.createElement("td");
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
row.appendChild(totalAppliedValueCell);
return nameCell;
}
static createTableCell(content) {
const cell = document.createElement("td");
cell.textContent = content;
return cell;
}
static createTotalCostCell(obj) {
const totalCostCell = document.createElement("td");
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
row.appendChild(totalCostCell);
return totalCostCell;
}
static createFlexContainerCell(obj, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("flex-container");
const addButton = CostUI.createAddButton(obj, callbacks);
const selectButton = CostUI.createSelectButton(obj, callbacks);
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
return flexContainerCell;
}
static createAddButton(obj, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
@@ -329,31 +352,17 @@ export class CostUI {
e.stopPropagation();
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
});
return addButton;
}
static createSelectButton(obj, callbacks) {
const selectButton = document.createElement("button");
selectButton.textContent = "Select";
selectButton.addEventListener("click", function(e) {
e.stopPropagation();
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
});
divFlex.appendChild(addButton);
divFlex.appendChild(selectButton);
const flexContainerCell = document.createElement("td");
flexContainerCell.appendChild(divFlex);
row.appendChild(flexContainerCell);
row.get_id = function() {
return this.getAttribute("id");
};
row.get_parent = function() {
const parentId = this.getAttribute("parent-id");
return parentId ? document.getElementById(parentId) : null;
};
return row;
return selectButton;
}
static hideNestedRows(parentId) {
@@ -461,4 +470,267 @@ export class CostUI {
return card;
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
// Check if the form already exists and remove it if it does
const formId = "cost-values-form-" + costItemId;
let existingForm = document.getElementById(formId);
if (existingForm) {
existingForm.remove();
}
const form = CostUI.createFormElement(formId, "cost-values-form");
const header = CostUI.createHeader("Cost Values Form");
form.appendChild(header);
const table = CostUI.createCostValuesTable(costItemId, ["Type", "Category", "Value"]);
costValues.forEach(costValue => {
const tr = CostUI.createCostvaluesRow(costItemId, costValue);
table.appendChild(tr);
});
form.appendChild(table);
const addButton = CostUI.createAddCostValueButton(costItemId, callbacks);
form.appendChild(addButton);
const submitButton = CostUI.createSubmitButton(callbacks);
form.appendChild(submitButton);
const closeButton = CostUI.createCloseButton(form);
form.appendChild(closeButton);
CostUI.makeHeaderDraggable(header, form);
document.body.appendChild(form);
return form;
}
static getCostValuesTable(costItemId) {
return document.getElementById("cost-values-table-" + costItemId);
}
static addNewCostValueRow(costItemId, costValueId) {
const table = CostUI.getCostValuesTable(costItemId);
// check if table exists
if (!table) {
console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
console.log("Adding new cost value row", costValueId);
console.log(table)
const tr = CostUI.createCostvaluesRow(costItemId, {"category": "", "name": "", "applied_value": 0, "id": costValueId, "parent": costItemId});
table.appendChild(tr);
}
static createCostValuesTable(costItemId, headers) {
const table = document.createElement("table");
table.classList.add("cost-values-table");
table.id = "cost-values-table-" + costItemId;
const headerRow = document.createElement("tr");
headers.forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
headerRow.appendChild(th);
});
table.appendChild(headerRow);
return table;
}
static createCostvaluesRow(costItemId,costValue) {
function cleanLabel(value) {
// remove anything which is not a dot or a digit
return value.replace(/[^0-9.]/g, '');
}
if (document.getElementById(costValue.id)) {
return document.getElementById(costValue.id);
}
const tr = document.createElement("tr");
tr.isCostValue = true;
tr.parent = costItemId;
tr.id = costValue.id;
let costType = "FIXED";
if (costValue.category === "*") {
costType = "SUM";
}
else if (costValue.category && costValue.category !== "*") {
costType = "CATEGORY";
}
else if (costValue.applied_value) {
costType = "FIXED";
}
const typeCell = CostUI.createTableDropdown("type", costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function() {
const selectedType = this.value;
CostUI.updateRowBasedOnType(selectedType, categoryCell, valueCell1);
});
tr.appendChild(typeCell);
const categoryCell = CostUI.createTableInput("text", "category", costValue.category);
tr.appendChild(categoryCell);
let value
if (costValue.category === "*"){
value = cleanLabel(costValue.label)
}
else {
value = costValue.applied_value
}
const valueCell1 = CostUI.createTableInput("number", "value", value);
tr.appendChild(valueCell1);
// Apply initial state based on costType
CostUI.updateRowBasedOnType(costType, categoryCell, valueCell1);
return tr;
}
static updateRowBasedOnType(type, categoryCell, valueCell1) {
if (type === "FIXED") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "CATEGORY") {
categoryCell.querySelector("input").disabled = false;
categoryCell.querySelector("input").style.backgroundColor = "";
valueCell1.querySelector("input").disabled = false;
valueCell1.querySelector("input").style.backgroundColor = "";
} else if (type === "SUM") {
categoryCell.querySelector("input").disabled = true;
categoryCell.querySelector("input").style.backgroundColor = "lightgrey";
valueCell1.querySelector("input").disabled = true;
valueCell1.querySelector("input").style.backgroundColor = "lightgrey";
}
}
static createTableDropdown(name, value="FIXED") {
const cell = document.createElement("td");
const dropdown = document.createElement("select");
dropdown.name = name;
const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach(optionValue => {
const option = document.createElement("option");
option.value = optionValue;
option.textContent = optionValue;
if (optionValue === value) {
option.selected = true;
}
dropdown.appendChild(option);
});
cell.appendChild(dropdown);
return cell;
}
static createTableInput(type, name, value) {
const cell = document.createElement("td");
const input = document.createElement("input");
input.type = type;
input.name = name;
input.value = value;
cell.appendChild(input);
return cell;
}
static createAddCostValueButton(costItemId, callbacks) {
const addButton = document.createElement("button");
addButton.textContent = "+";
addButton.classList.add("add-button");
addButton.addEventListener("click", function(e) {
e.preventDefault();
callbacks.addCostValue ? callbacks.addCostValue(costItemId) : null;
});
return addButton;
}
static createSubmitButton(callbacks) {
const submitButton = document.createElement("button");
submitButton.type = "submit";
submitButton.textContent = "Save";
submitButton.addEventListener("click", function(e) {
e.preventDefault();
const form = this.closest("form");
const costValues = [];
const rows = Array.from(form.querySelectorAll("tr")).filter(row => row.isCostValue);
rows.forEach(row => {
const costCategory = row.querySelector("input[name='category']").value;
const appliedValue = parseFloat(row.querySelector("input[name='value']").value);
const costType = row.querySelector("select[name='type']").value;
const id = parseInt(row.id);
costValues.push({ costType, costCategory, appliedValue, id: id, costItemId: row.parent });
});
const costItemId = parseInt(form.id.split("-")[3]);
console.log(form.id)
console.log("Cost values to be saved:", costItemId);
callbacks.editCostValues ? callbacks.editCostValues(costItemId, costValues) : null;
form.remove();
});
return submitButton;
}
static createFormElement(id, className) {
const form = document.createElement("form");
form.id = id;
form.classList.add(className);
form.style.position = "absolute"; // Set initial position to absolute
form.style.top = "50%";
form.style.left = "50%";
form.style.transform = "translate(-50%, -50%)";
return form;
}
static createHeader(text) {
const header = document.createElement("div");
header.classList.add("form-header");
header.textContent = text;
return header;
}
static createCloseButton(form) {
const closeButton = document.createElement("span");
closeButton.classList.add("close-button");
closeButton.innerHTML = "&times;";
closeButton.addEventListener("click", function() {
form.remove();
});
return closeButton;
}
static makeHeaderDraggable(header, form) {
header.addEventListener("mousedown", function(e) {
let offsetX = e.clientX - form.getBoundingClientRect().left;
let offsetY = e.clientY - form.getBoundingClientRect().top;
function mouseMoveHandler(e) {
form.style.left = `${e.clientX - offsetX}px`;
form.style.top = `${e.clientY - offsetY}px`;
}
function mouseUpHandler() {
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
}
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
});
}
}
@@ -4,8 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlenderBIM Web UI</title>
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
<link rel="stylesheet" href="/static/css/components/card.css" />
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
<link
rel="stylesheet"
id="tabulator-stylesheet"
+12 -2
View File
@@ -715,9 +715,17 @@ class IfcImporter:
if not products:
return results
if tool.Loader.settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
iterator = ifcopenshell.geom.iterator(
settings,
self.file,
multiprocessing.cpu_count(),
include=products,
geometry_library=self.ifc_import_settings.geometry_library,
)
else:
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
iterator = ifcopenshell.geom.iterator(
settings, self.file, include=products, geometry_library=self.ifc_import_settings.geometry_library
)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
@@ -1473,6 +1481,7 @@ class IfcImportSettings:
self.logger: logging.Logger = None
self.input_file = None
self.diff_file = None
self.geometry_library = "opencascade"
self.should_use_cpu_multiprocessing = True
self.should_merge_materials_by_colour = False
self.should_load_geometry = True
@@ -1508,6 +1517,7 @@ class IfcImportSettings:
logger = logging.getLogger("ImportIFC")
settings.logger = logger
settings.diff_file = scene_diff.diff_json_file
settings.geometry_library = props.geometry_library
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
settings.should_load_geometry = props.should_load_geometry
@@ -79,6 +79,7 @@ classes = (
operator.SelectUnassignedProducts,
operator.UnassignCostItemQuantity,
operator.UnassignCostItemType,
operator.GenerateCostScheduleBrowser,
prop.CostItem,
prop.CostItemQuantity,
prop.CostItemType,
+2 -8
View File
@@ -265,16 +265,10 @@ class CostSchedulesData:
@classmethod
def cost_values(cls):
results = []
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
if not ifc_id:
return results
cost_item = tool.Ifc.get().by_id(ifc_id)
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
return results
return []
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
@classmethod
def quantity_types(cls):
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
core.add_currency(tool.Ifc, tool.Cost)
class GenerateCostScheduleBrowser(bpy.types.Operator):
bl_idname = "bim.generate_cost_schedule_browser"
bl_label = "Generate Cost Schedule Browser"
bl_options = {"REGISTER", "UNDO"}
cost_schedule: bpy.props.IntProperty()
def execute(self, context):
core.generate_cost_schedule_browser(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
return {"FINISHED"}
+2 -1
View File
@@ -73,7 +73,8 @@ class BIM_PT_cost_schedules(Panel):
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.export_cost_schedules", text="Export spreadsheet", icon="EXPORT").cost_schedule = cost_schedule["id"]
row1.operator("bim.generate_cost_schedule_browser", text="Generate spreadsheet browsser", icon="URL").cost_schedule = cost_schedule["id"]
row2 = col.row(align=True)
row2.alignment = "RIGHT"
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
@@ -204,6 +204,8 @@ def format_distance(
tx_dist = ""
if feet is not None:
tx_dist += str(feet) + "'"
if not feet and not add_inches:
tx_dist += str(feet) + "'"
if feet and add_inches:
tx_dist += " - "
if not feet and value < 0:
+270 -20
View File
@@ -17,7 +17,6 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import bpy
import json
import time
@@ -43,13 +42,12 @@ import bonsai.core.drawing as core
import bonsai.bim.module.drawing.svgwriter as svgwriter
import bonsai.bim.module.drawing.annotation as annotation
import bonsai.bim.module.drawing.sheeter as sheeter
import bonsai.bim.module.drawing.scheduler as scheduler
import bonsai.bim.module.drawing.helper as helper
import bonsai.bim.export_ifc
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
from typing import NamedTuple, List, Union, Optional, Literal
from lxml import etree
from math import radians
from mathutils import Vector, Color, Matrix
from timeit import default_timer as timer
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
@@ -204,7 +202,12 @@ class CreateDrawing(bpy.types.Operator):
@classmethod
def poll(cls, context):
return bool(tool.Ifc.get() and tool.Drawing.is_drawing_active())
if not tool.Ifc.get():
return False
if not tool.Drawing.is_drawing_active():
cls.poll_message_set("No active drawing.")
return False
return True
def invoke(self, context, event):
# printing all drawings on shift+click
@@ -252,6 +255,7 @@ class CreateDrawing(bpy.types.Operator):
self.svg_writer.camera_projection = tuple(
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
)
self.svg_writer.calculate_scale()
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
@@ -264,7 +268,12 @@ class CreateDrawing(bpy.types.Operator):
with profile("Generate linework"):
if tool.Drawing.is_camera_orthographic():
linework_svg = self.generate_linework(context)
if self.camera.data.BIMCameraProperties.linework_mode == "OPENCASCADE":
linework_svg = self.generate_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
elif self.camera.data.BIMCameraProperties.linework_mode == "FREESTYLE":
linework_svg = self.generate_freestyle_linework(context)
with profile("Generate annotation"):
if tool.Drawing.is_camera_orthographic():
@@ -471,11 +480,35 @@ class CreateDrawing(bpy.types.Operator):
geom_settings = ifcopenshell.geom.settings()
geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
# See bug 5231 - offset no longer available in v0.8.0
absolute_placements = set()
placement_replacements = {}
if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view:
offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# A 2mm Z offset to combat Z-fighting in plan or RCPs
offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
geom_settings.offset = offset
offset_z = 0.002 if target_view == "PLAN_VIEW" else -0.002
for product in self.file.by_type("IfcProduct"):
if not product.ObjectPlacement:
continue
absolute_placement = self.get_absolute_placement(product.ObjectPlacement)
if absolute_placement.is_a("IfcLocalPlacement"):
absolute_placements.add(absolute_placement)
transformation = np.eye(4)
transformation[2][3] = offset_z
# Don't use undo system in case we bork up a parent caller
for placement in absolute_placements:
old = placement.RelativePlacement
new = self.get_relative_placement(
ifc, transformation @ ifcopenshell.util.placement.get_local_placement(placement)
)
placement.RelativePlacement = new
placement_replacements[placement] = (old, new)
# offset = ifcopenshell.ifcopenshell_wrapper.float_array_3()
# offset[2] = 0.002 if target_view == "PLAN_VIEW" else -0.002
# geom_settings.offset = offset
geom_settings.set("context-ids", context)
it = ifcopenshell.geom.iterator(
geom_settings, ifc, multiprocessing.cpu_count(), include=drawing_elements
@@ -487,6 +520,186 @@ class CreateDrawing(bpy.types.Operator):
tree.add_element(elem)
drawing_elements -= processed
for placement, oldnew in placement_replacements.items():
old, new = oldnew
placement.RelativePlacement = old
ifcopenshell.util.element.remove_deep2(ifc, new)
placement_replacements = {}
def get_absolute_placement(self, object_placement):
if object_placement.PlacementRelTo:
return self.get_absolute_placement(object_placement.PlacementRelTo)
return object_placement
def get_relative_placement(self, ifc, m):
x = np.array((m[0][0], m[1][0], m[2][0]))
z = np.array((m[0][2], m[1][2], m[2][2]))
o = np.array((m[0][3], m[1][3], m[2][3]))
object_matrix = ifcopenshell.util.placement.a2p(o, z, x)
return self.create_ifc_axis_2_placement_3d(
ifc,
object_matrix[:, 3][0:3],
object_matrix[:, 2][0:3],
object_matrix[:, 0][0:3],
)
def create_ifc_axis_2_placement_3d(self, ifc, point, up, forward):
return self.file.createIfcAxis2Placement3D(
self.file.createIfcCartesianPoint(point.tolist()),
self.file.createIfcDirection(up.tolist()),
self.file.createIfcDirection(forward.tolist()),
)
def generate_bisect_linework(self, context: bpy.types.Context, root):
camera_matrix_i = context.scene.camera.matrix_world.inverted()
group = root.find("{http://www.w3.org/2000/svg}g")
raw_width, raw_height = self.get_camera_dimensions()
x_offset = raw_width / 2
y_offset = raw_height / 2
svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm
for obj in context.visible_objects:
if obj.type != "MESH":
continue
if not (element := tool.Ifc.get_entity(obj)):
continue
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
continue
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g")
g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId
g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or ""
lines = []
for edge in edges:
start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy]
end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy]
coords = [start, end]
d = " ".join(
["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords]
)
d = "M{}".format(d[1:])
path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path")
path.attrib["d"] = d
group.append(g)
def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
svg_path = self.get_svg_path(cache_type="linework")
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
return svg_path
context.scene.render.engine = "BLENDER_WORKBENCH"
context.scene.render.use_freestyle = True
context.scene.svg_export.use_svg_export = True
linesets = context.view_layer.freestyle_settings.linesets
if len(linesets) == 1 and linesets[0].name == "LineSet":
context.view_layer.freestyle_settings.crease_angle = radians(140)
context.view_layer.freestyle_settings.use_culling = True
lineset = linesets[0]
lineset.edge_type_negation = "EXCLUSIVE"
lineset.select_silhouette = False
lineset.select_crease = False
lineset.select_border = False
lineset.select_edge_mark = False
lineset.select_contour = False
lineset.select_external_contour = False
lineset.select_material_boundary = False
lineset.select_suggestive_contour = True
lineset.select_ridge_valley = True
edge_mesh = bpy.data.meshes.new("Temp Merged Edges")
edge_obj = bpy.data.objects.new("Temp Merged Edges", edge_mesh)
context.scene.collection.objects.link(edge_obj)
edge_bm = bmesh.new()
visible_object_names = {obj.name for obj in bpy.context.visible_objects}
for obj in bpy.context.view_layer.objects:
is_visible = obj.name in visible_object_names
obj.hide_render = not is_visible
if (
is_visible
and obj.type == "MESH"
and len(obj.data.edges)
and not len(obj.data.polygons)
and not obj.name.startswith("IfcAnnotation")
):
tmp_mesh = None
try:
tmp_mesh = obj.data.copy()
tmp_mesh.transform(obj.matrix_world)
edge_bm.from_mesh(tmp_mesh)
finally:
if tmp_mesh:
bpy.data.meshes.remove(tmp_mesh)
ret = bmesh.ops.extrude_edge_only(edge_bm, edges=edge_bm.edges)
verts_extruded = [e for e in ret["geom"] if isinstance(e, bmesh.types.BMVert)]
cam_z = self.camera.matrix_world.to_3x3() @ self.camera.data.view_frame(scene=None)[-1].normalized()
cam_z *= 0.001
for v in verts_extruded:
v.co += cam_z
edge_bm.to_mesh(edge_mesh)
edge_bm.free()
actual_path = svg_path[0:-4] + "0001.svg"
context.scene.render.filepath = svg_path[0:-4]
bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path)
bpy.data.objects.remove(edge_obj)
bpy.data.meshes.remove(edge_mesh)
context.scene.render.use_freestyle = False
context.scene.svg_export.use_svg_export = False
tree = etree.parse(svg_path)
root = tree.getroot()
freestyle_width = float(root.attrib["width"])
freestyle_height = float(root.attrib["height"])
svg_width = self.svg_writer.width
svg_height = self.svg_writer.height
group = root.find(".//{http://www.w3.org/2000/svg}g")
group.attrib["class"] = "projection"
# Resize Freestyle to our proper width / height and purge all other attributes
for path in root.findall(".//{http://www.w3.org/2000/svg}path"):
for key in path.attrib:
if key == "fill":
continue
elif key != "d":
del path.attrib[key]
continue
d = path.attrib[key]
coords = d.strip().split()[1:]
new_d = "M"
for i in range(0, len(coords), 2):
x = float(coords[i][:-1])
y = float(coords[i + 1])
x = x / freestyle_width * svg_width
y = y / freestyle_height * svg_height
new_d += f" {x},{y}"
path.attrib["d"] = new_d
pass
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
with open(svg_path, "wb") as svg:
svg.write(etree.tostring(root))
return svg_path
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
return
@@ -573,10 +786,15 @@ class CreateDrawing(bpy.types.Operator):
return svg_path
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
self.remove_cut_linework(root)
self.generate_bisect_linework(context, root)
self.merge_linework_and_add_metadata(root)
elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root)
self.merge_linework_and_add_metadata(root)
if self.camera.data.BIMCameraProperties.calculate_shapely_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SHAPELY":
# shapely variant
group = root.find("{http://www.w3.org/2000/svg}g")
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
@@ -661,7 +879,7 @@ class CreateDrawing(bpy.types.Operator):
path.set("class", " ".join(list(classes)))
group.insert(0, path)
if self.camera.data.BIMCameraProperties.calculate_svgfill_surfaces:
if self.camera.data.BIMCameraProperties.fill_mode == "SVGFILL":
results = etree.tostring(root).decode("utf8")
svg_data_1 = results
from xml.dom.minidom import parseString
@@ -826,7 +1044,6 @@ class CreateDrawing(bpy.types.Operator):
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
self.svg_writer.calculate_scale()
x_offset = self.svg_writer.raw_width / 2
y_offset = self.svg_writer.raw_height / 2
@@ -931,6 +1148,7 @@ class CreateDrawing(bpy.types.Operator):
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
for edge in bm.edges:
if not edge.is_manifold:
bm.free()
@@ -951,6 +1169,11 @@ class CreateDrawing(bpy.types.Operator):
except:
continue
def remove_cut_linework(self, root):
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
if "projection" not in el.get("class", "").split():
el.getparent().remove(el)
def merge_linework_and_add_metadata(self, root):
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
if join_criteria:
@@ -1014,10 +1237,34 @@ class CreateDrawing(bpy.types.Operator):
if results:
for path in old_paths:
path.getparent().remove(path)
# polygonize_full will create polygons for everything, including
# interior "holes". As a result we do two passes. The first pass
# records polygon interior rings. The second pass uses this to
# check if the exterior ring matches an interior ring. If it does,
# it's a hole. Skip it!
interior_hashes = set()
for result in results:
for geom in result.geoms:
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if isinstance(geom, shapely.Polygon):
for interior in geom.interiors:
# Sorted because coordinate ordering may differ,
# and frozenset because shapely sometimes emits
# duplicate coordinates.
interior_hashes.add(hash(frozenset(sorted(interior.coords))))
elif isinstance(geom, shapely.LineString):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
for result in results:
for geom in result.geoms:
if isinstance(geom, shapely.Polygon):
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
if hash(frozenset(sorted(geom.exterior.coords))) in interior_hashes:
# This is a "hole", as its exterior perfectly matches an interior.
continue
d = (
"M"
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
@@ -1029,9 +1276,7 @@ class CreateDrawing(bpy.types.Operator):
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
+ " Z"
)
elif isinstance(geom, shapely.LineString):
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
path.attrib["d"] = d
path.attrib["d"] = d
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
@@ -1570,7 +1815,9 @@ class ActivateDrawing(bpy.types.Operator):
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.activate_drawing_style()
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
CutDecorator.install(context)
tool.Drawing.show_decorations()
@@ -1711,7 +1958,10 @@ class ReloadDrawingStyles(bpy.types.Operator):
drawing_style.raster_style = json.dumps(style_data["raster_style"])
if current_style is not None:
camera_props.active_drawing_style_index = styles.index(current_style)
try:
camera_props.active_drawing_style_index = styles.index(current_style)
except ValueError:
self.report({"INFO"}, f"Could not find style {current_style} in EPset_Drawing.ShadingStyles.")
return {"FINISHED"}
@@ -2883,5 +3133,5 @@ class OpenDocumentationWebUi(bpy.types.Operator):
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="documentation")
else:
bpy.ops.bim.bim.open_web_browser(page="documentation")
bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"}
+25 -2
View File
@@ -390,8 +390,31 @@ class DocProperties(PropertyGroup):
class BIMCameraProperties(PropertyGroup):
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
linework_mode: EnumProperty(
items=[
("OPENCASCADE", "OpenCASCADE", "Slower, more accurate, with more features"),
("FREESTYLE", "Freestyle", "Faster, less accurate, no fill support"),
],
default="OPENCASCADE",
name="Linework Mode",
)
fill_mode: EnumProperty(
items=[
("NONE", "None", "Disable filling areas seen in projection"),
("SHAPELY", "Shapely", "Recommended"),
("SVGFILL", "SVGFill", "Experimental"),
],
default="NONE",
name="Fill Mode",
)
cut_mode: EnumProperty(
items=[
("BISECT", "Bisect", "Faster, more forgiving to bad geometry"),
("OPENCASCADE", "OpenCASCADE", "More technically correct"),
],
default="BISECT",
name="Cut Mode",
)
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
+9 -3
View File
@@ -64,9 +64,15 @@ class BIM_PT_camera(Panel):
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
row = self.layout.row()
row.prop(props, "calculate_shapely_surfaces")
row = self.layout.row()
row.prop(props, "calculate_svgfill_surfaces")
row.prop(props, "linework_mode")
if props.linework_mode == "OPENCASCADE":
row = self.layout.row()
row.prop(props, "fill_mode")
row = self.layout.row()
row.prop(props, "cut_mode")
elif not hasattr(context.scene, "svg_export"):
row = self.layout.row()
row.label(text="Freestyle SVG Exporter Not Installed", icon="ERROR")
row = self.layout.row()
row.prop(props, "width")
@@ -1654,8 +1654,9 @@ class OverrideModeSetEdit(bpy.types.Operator):
should_sync_changes_first=False,
apply_openings=False,
)
tool.Geometry.apply_item_ids_as_vertex_groups(obj)
tool.Geometry.dissolve_triangulated_edges(obj)
if isinstance(obj.data, bpy.types.Mesh):
tool.Geometry.apply_item_ids_as_vertex_groups(obj)
tool.Geometry.dissolve_triangulated_edges(obj)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
else:
obj.select_set(False)
@@ -39,6 +39,10 @@ classes = (
operator.ViewFromSun,
operator.RefreshIFCMaterials,
operator.UnmapMaterial,
operator.RADIANCE_OT_select_camera,
operator.RADIANCE_OT_export_material_mappings,
operator.RADIANCE_OT_import_material_mappings,
operator.RADIANCE_OT_open_spectraldb,
prop.RadianceMaterial,
prop.BIMSolarProperties,
prop.RadianceExporterProperties,
@@ -1,3 +1,22 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import os
@@ -10,6 +29,15 @@ class MATERIAL_UL_radiance_materials(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
# Adding color preview or glass icon
if item.category == "Glass":
row.label(icon="SHADING_TEXTURE")
else:
color_rect = row.row()
color_rect.prop(item, "color", text="")
color_rect.scale_x = 0.3
row.prop(item, "name", text="", emboss=False, icon_value=icon)
if item.is_mapped:
row.label(text=f"{item.category} - {item.subcategory}")
+197 -55
View File
@@ -30,11 +30,16 @@ import bonsai.tool as tool
from pathlib import Path
from typing import Union, Optional, Sequence
import json
import math
import time
import ifcopenshell
import webbrowser
import ifcopenshell.geom
import multiprocessing
from mathutils import Vector
from bonsai.bim.module.light.data import SolarData
from bpy_extras.io_utils import ExportHelper
from bpy_extras.io_utils import ImportHelper
ifc_materials = []
@@ -95,14 +100,10 @@ class ExportOBJ(bpy.types.Operator):
while True:
shape = iterator.get()
materials = shape.geometry.materials
# print(shape.geometry)
material_ids = shape.geometry.material_ids
# material_names = shape.geometry.material_names
# print(material_ids)
for material in materials:
# print(material, dir(material))
# print(material.name)
ifc_materials.append(material.name)
serialiser.write(shape)
@@ -129,10 +130,15 @@ class RadianceRender(bpy.types.Operator):
self.report({"ERROR"}, "PyRadiance is not available. Cannot perform rendering.")
return {"CANCELLED"}
# Get the resolution from the user input
print("Starting Radiance rendering process...")
props = context.scene.radiance_exporter_properties
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
context.scene.render.resolution_x = resolution_x
context.scene.render.resolution_y = resolution_y
aspect_ratio = resolution_x / resolution_y
quality = props.radiance_quality.upper()
detail = props.radiance_detail.upper()
variability = props.radiance_variability.upper()
@@ -141,6 +147,11 @@ class RadianceRender(bpy.types.Operator):
output_file_format = props.output_file_format
use_hdr = props.use_hdr
choose_hdr_image = props.choose_hdr_image
print(f"Resolution: {resolution_x}x{resolution_y}")
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
print(f"Output directory: {output_dir}")
if use_hdr:
hdr_image = "noon_grass_2k.hdr"
hdr_mask = "noon_grass_2k_mask.hdr"
@@ -148,30 +159,34 @@ class RadianceRender(bpy.types.Operator):
hdr_image_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_image)
hdr_mask_path = os.path.join(os.path.dirname(__file__), "HDRs", hdr_mask)
sky_map_cal_path = os.path.join(os.path.dirname(__file__), "HDRs", sky_map_cal)
# os.chdir(output_dir)
obj_file_path = os.path.join(output_dir, "model.obj")
sun_props = context.scene.BIMSolarProperties
sun_pos_props = context.scene.sun_pos_properties
sky_file_path = os.path.join(output_dir, "sky.rad")
latitude = sun_props.latitude
longitude = sun_props.longitude
timezone = sun_props.timezone
month = sun_props.month
day = sun_props.day
hour = sun_props.hour
minute = sun_props.minute
# latitude = sun_props.latitude
# longitude = sun_props.longitude
# month = sun_props.month
# day = sun_props.day
# hour = sun_props.hour
# minute = sun_props.minute
print("Sun Properties:")
print("Latitude: ", latitude)
print("Longitude: ", longitude)
print("Timezone: ", timezone)
print("Month: ", month)
print("Day: ", day)
print("Hour: ", hour)
print("Minute: ", minute)
# print("Sun Properties:")
# print("Latitude: ", latitude)
# print("Longitude: ", longitude)
# print("Timezone: ", timezone)
# print("Month: ", month)
# print("Day: ", day)
# print("Hour: ", hour)
# print("Minute: ", minute)
print("Setting up camera...")
if props.use_active_camera:
camera = context.scene.camera
else:
camera = props.selected_camera
camera = self.get_active_camera(context)
if camera is None:
self.report({"ERROR"}, "No active camera found in the scene. Please add a camera and set it as active.")
return {"CANCELLED"}
@@ -179,19 +194,35 @@ class RadianceRender(bpy.types.Operator):
# Get camera position and direction
camera_position, camera_direction = self.get_camera_data(camera)
dt = datetime(2024, month, day, hour, minute)
print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}")
# sun_position = tool.Blender.get_sun_position_addon()
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time,
# sun_pos_props.latitude,
# sun_pos_props.longitude,
# -sun_pos_props.UTC_zone,
# sun_pos_props.month,
# sun_pos_props.day,
# sun_pos_props.year,
# )
dt = datetime(sun_pos_props.year, sun_props.month, sun_props.day, sun_props.hour, sun_props.minute)
sky_description = pr.gensky(
dt=dt,
latitude=latitude,
longitude=longitude,
timezone=timezone,
year=2024,
sunny_with_sun=True,
sunny_without_sun=False,
cloudy=False,
ground_reflectance=0.2,
turbidity=3.0,
# azimuth=64.1,
# altitude=-26.6,
latitude=sun_props.latitude,
longitude=sun_props.longitude,
year=sun_pos_props.year,
timezone=-int(sun_props.UTC_zone),
# sunny_with_sun=False,
# sunny_without_sun=False,
# cloudy=False,
# ground_reflectance=0.2,
# turbidity=3.0,
)
sky_description_str = sky_description.decode("utf-8")
@@ -292,11 +323,7 @@ ground_glow source ground
props = context.scene.radiance_exporter_properties
if props.use_json_file:
with open(props.json_file, "r") as file:
data = json.load(file)
else:
data = props.get_mappings_dict()
data = props.get_mappings_dict()
materials_file = os.path.join(output_dir, "materials.rad")
written_materials = set()
@@ -318,9 +345,6 @@ ground_glow source ground
file.write(material)
written_materials.add(material.split()[2]) # Add material name to written set
print(data)
print(default_materials)
for style_id in all_materials:
material = next((m for m in props.materials if m.style_id == style_id), None)
if material and material.is_mapped:
@@ -351,7 +375,7 @@ ground_glow source ground
self.report({"INFO"}, "Exported Scene file to: {}".format(scene_file))
# Py Radiance Rendering code
print("Setting up Radiance scene...")
scene = pr.Scene("ascene")
material_path = os.path.join(output_dir, "materials.rad")
@@ -360,10 +384,39 @@ ground_glow source ground
scene.add_material(material_path)
scene.add_surface(scene_path)
scene.add_source(sky_file_path)
print("Setting up view...")
if camera.data.type == "PERSP":
# Perspective camera
camera_fov = camera.data.angle
# Calculate vertical FOV based on the desired aspect ratio
vertical_fov = 2 * math.atan(math.tan(camera_fov / 2) / aspect_ratio)
aview = pr.View(position=camera_position, direction=camera_direction)
aview = pr.View(
vtype="v", # Perspective view
position=camera_position,
direction=camera_direction,
vup=(0, 0, 1), # Assuming Z is up
horiz=math.degrees(camera_fov),
vert=math.degrees(vertical_fov),
)
else: # 'ORTHO'
# Orthographic camera
# Calculate the view size based on the camera's orthographic scale
ortho_scale = camera.data.ortho_scale
view_width = ortho_scale
view_height = ortho_scale / aspect_ratio
aview = pr.View(
vtype="l", # Parallel projection (orthographic)
position=camera_position,
direction=camera_direction,
vup=(0, 0, 1), # Assuming Z is up
horiz=view_width,
vert=view_height,
)
scene.add_view(aview)
print("Starting render...")
start_time = time.time()
image = pr.render(
scene,
ambbounce=1,
@@ -371,32 +424,34 @@ ground_glow source ground
quality=quality,
detail=detail,
variability=variability,
nproc=multiprocessing.cpu_count(),
)
end_time = time.time()
print(f"Render completed in {end_time - start_time:.2f} seconds")
output_hdr_path = os.path.join(output_dir, f"{output_file_name}.{output_file_format.lower()}")
print(f"Saving HDR output to: {output_hdr_path}")
if output_file_format == "HDR":
with open(output_hdr_path, "wb") as wtr:
wtr.write(image)
else:
pass
print("Applying tone mapping...")
pcond_image = pr.pcond(hdr=output_hdr_path, human=True)
tiff_path = os.path.join(output_dir, f"{output_file_name}.tiff")
print(f"Saving TIFF output to: {tiff_path}")
pr.ra_tiff(inp=pcond_image, out=tiff_path, lzw=True)
print("Radiance rendering process completed successfully.")
self.report({"INFO"}, "Radiance rendering completed. Output: {}".format(tiff_path))
return {"FINISHED"}
def get_active_camera(self, context):
if context.scene.camera:
props = context.scene.radiance_exporter_properties
if props.use_active_camera:
return context.scene.camera
for obj in context.scene.objects:
if obj.type == "CAMERA":
return obj
return None
else:
return props.selected_camera
def get_camera_data(self, camera):
# Get camera position
@@ -509,9 +564,29 @@ class RefreshIFCMaterials(bpy.types.Operator):
for render_item in style.Styles:
if render_item.is_a("IfcSurfaceStyleRendering"):
style_id = f"IfcSurfaceStyleRendering-{render_item.id()}"
ifc_materials.append(style_id)
style_name = style.Name or f"Unnamed Style {render_item.id()}"
props.add_material_mapping(style_id, style_name)
# Extract color and transparency
color = (1.0, 1.0, 1.0) # Default white
transparency = 0.0 # Default opaque
if render_item.SurfaceColour:
color = (
render_item.SurfaceColour.Red,
render_item.SurfaceColour.Green,
render_item.SurfaceColour.Blue,
)
if hasattr(render_item, "Transparency") and render_item.Transparency is not None:
transparency = render_item.Transparency
# Add material with color
material = props.add_material_mapping(style_id, style_name)
material.color = color
# If transparency is high, consider it as glass
if transparency > 0.5:
material.category = "Glass"
material.subcategory = "Clear Glass"
material.is_mapped = True
props.active_material_index = 0 if props.materials else -1
@@ -531,3 +606,70 @@ class UnmapMaterial(bpy.types.Operator):
material = props.materials[self.material_index]
props.unmap_material(material.name)
return {"FINISHED"}
class RADIANCE_OT_select_camera(bpy.types.Operator):
bl_idname = "radiance.select_camera"
bl_label = "Select Camera"
bl_description = "Select a camera from the viewport"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object is not None and context.object.type == "CAMERA"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props.selected_camera = context.object
props.use_active_camera = False
return {"FINISHED"}
class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
bl_idname = "radiance.export_material_mappings"
bl_label = "Export Material Mappings"
bl_description = "Export material mappings to a JSON file"
filename_ext = ".json"
def execute(self, context):
props = context.scene.radiance_exporter_properties
mappings = {}
for material in props.materials:
if material.is_mapped:
mappings[material.style_id] = {
"name": material.name,
"category": material.category,
"subcategory": material.subcategory,
}
with open(self.filepath, "w") as f:
json.dump(mappings, f, indent=4)
self.report({"INFO"}, f"Material mappings exported to {self.filepath}")
return {"FINISHED"}
class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper):
bl_idname = "radiance.import_material_mappings"
bl_label = "Import Material Mappings"
bl_description = "Import material mappings from a JSON file"
filename_ext = ".json"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props.import_mappings(self.filepath)
self.report({"INFO"}, f"Material mappings imported from {self.filepath}")
return {"FINISHED"}
class RADIANCE_OT_open_spectraldb(bpy.types.Operator):
bl_idname = "radiance.open_spectraldb"
bl_label = "Open SpectralDB"
bl_description = "Open the SpectralDB website for reference"
def execute(self, context):
webbrowser.open("https://spectraldb.com")
return {"FINISHED"}
+46 -20
View File
@@ -32,6 +32,7 @@ from bpy.props import (
FloatVectorProperty,
BoolProperty,
CollectionProperty,
PointerProperty,
)
import os
from bpy.types import PropertyGroup
@@ -107,6 +108,11 @@ def update_display_sun_path(self, context):
SolarDecorator.uninstall()
def update_resolution(self, context):
context.scene.render.resolution_x = self.radiance_resolution_x
context.scene.render.resolution_y = self.radiance_resolution_y
def update_sun_path():
if not SolarData.is_loaded:
SolarData.load()
@@ -142,6 +148,10 @@ def update_sun_path():
rotation_euler = Euler((elevation - pi / 2, 0, -azimuth))
rotation_quaternion = rotation_euler.to_quaternion()
props.azimuth = azimuth
props.elevation = elevation
props.UTC_zone = zone
if sun_vector.z < 0:
bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1))
else:
@@ -162,14 +172,11 @@ class RadianceMaterial(PropertyGroup):
category: StringProperty(name="Category")
subcategory: StringProperty(name="Subcategory")
is_mapped: BoolProperty(name="Is Mapped", default=False)
color: FloatVectorProperty(name="Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, size=3)
class RadianceExporterProperties(PropertyGroup):
def update_json_file(self, context):
if self.json_file:
self.json_file = bpy.path.abspath(self.json_file)
def update_output_dir(self, context):
if self.output_dir:
self.output_dir = bpy.path.abspath(self.output_dir)
@@ -184,8 +191,26 @@ class RadianceExporterProperties(PropertyGroup):
item.style_id = style_id
item.category = ""
item.subcategory = ""
item.color = (1.0, 1.0, 1.0) # Default white
return item
def import_mappings(self, filepath):
with open(filepath, "r") as f:
mappings = json.load(f)
for style_id, mapping in mappings.items():
material = self.get_material_mapping(mapping["name"])
if material:
material.style_id = style_id
material.category = mapping["category"]
material.subcategory = mapping["subcategory"]
material.is_mapped = True
else:
new_material = self.add_material_mapping(style_id, mapping["name"])
new_material.category = mapping["category"]
new_material.subcategory = mapping["subcategory"]
new_material.is_mapped = True
def get_material_mapping(self, style_name):
return next((item for item in self.materials if item.name == style_name), None)
@@ -213,12 +238,6 @@ class RadianceExporterProperties(PropertyGroup):
item.subcategory = ""
item.is_mapped = False
use_json_file: BoolProperty(
name="Upload JSON",
description="Toggle between uploading a JSON file and using in-UI material mapping",
default=False,
)
is_exporting: bpy.props.BoolProperty(
name="Is Exporting", description="Whether the OBJ export is in progress", default=False
)
@@ -237,6 +256,7 @@ class RadianceExporterProperties(PropertyGroup):
("Plant", "Plant", ""),
("Exterior", "Exterior", ""),
("Color Swatch", "Color Swatch", ""),
("Glass", "Glass", ""),
]
def update_material_mapping(self, context):
@@ -269,14 +289,13 @@ class RadianceExporterProperties(PropertyGroup):
should_load_from_memory: BoolProperty(
name="Load from Memory",
default=False,
description="Use IFC file currently loaded in Bonsai",
)
radiance_resolution_x: IntProperty(
name="X", description="Horizontal resolution of the output image", default=1920, min=1
name="X", description="Horizontal resolution of the output image", default=1920, min=1, update=update_resolution
)
radiance_resolution_y: IntProperty(
name="Y", description="Vertical resolution of the output image", default=1080, min=1
name="Y", description="Vertical resolution of the output image", default=1080, min=1, update=update_resolution
)
output_dir: StringProperty(
name="Output Directory",
@@ -292,13 +311,6 @@ class RadianceExporterProperties(PropertyGroup):
subtype="FILE_PATH",
update=lambda self, context: self.update_ifc_file(context),
)
json_file: StringProperty(
name="JSON File",
description="Path to the JSON file",
default="",
subtype="FILE_PATH",
update=lambda self, context: self.update_json_file(context),
)
radiance_quality: EnumProperty(
name="Quality",
@@ -350,6 +362,17 @@ class RadianceExporterProperties(PropertyGroup):
default="Noon",
)
use_active_camera: BoolProperty(
name="Use Active Camera", description="Use the active camera in the scene", default=True
)
selected_camera: PointerProperty(
type=bpy.types.Object,
name="Camera",
description="Select a camera to use for rendering",
poll=lambda self, object: object.type == "CAMERA",
)
class BIMSolarProperties(PropertyGroup):
sites: EnumProperty(items=get_sites, name="Sites")
@@ -364,6 +387,9 @@ class BIMSolarProperties(PropertyGroup):
sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0))
sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0))
sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size)
azimuth: FloatProperty(name="Azimuth")
elevation: FloatProperty(name="Elevation")
UTC_zone: FloatProperty(name="UTC Zone")
display_shadows: BoolProperty(
name="Display Shadows",
default=False,
@@ -1,4 +1,7 @@
{
"Glass": {
"Clear Glass": "void BRTDfunc clear_glass\n10\n sr_clear_r sr_clear_g sr_clear_b\n st_clear_r st_clear_g st_clear_b\n 0 0 0\n glaze1.cal\n0\n19\n 0 0 0\n 0 0 0\n 0 0 0\n 1\n 0.074 0.077 0.079\n 0.074 0.077 0.079\n 0.862 0.890 0.886\n"
},
"Wall": {
"White Painted Room Walls": "void plastic white_painted_room_walls \n0\n0\n5 0.8316 0.8116 0.7226 0.0036 0.2",
"White Painted Corridor Walls": "void plastic white_painted_corridor_walls \n0\n0\n5 0.8143 0.7984 0.715 0.0039 0.2",
+41 -33
View File
@@ -50,43 +50,52 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row.prop(props, "output_dir")
row = layout.row()
layout.prop(props, "use_json_file")
layout.label(text="Info: Unmapped materials default to white")
row = layout.row()
row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index")
row.operator("radiance.open_spectraldb", text="", icon="WORLD") # Globe icon
if len(props.materials) > 0:
col = layout.column(align=True)
col.prop(props, "category")
if props.category:
col.prop(props, "subcategory")
if props.use_json_file:
row = layout.row()
row.prop(props, "json_file")
if props.active_material_index >= 0 and props.active_material_index < len(props.materials):
active_material = props.materials[props.active_material_index]
if active_material.category and active_material.subcategory:
layout.label(
text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}"
)
else:
layout.label(text=f"Select category and subcategory for: {active_material.name}")
if not props.use_json_file:
row = layout.row()
layout.label(text="Info: Unmapped materials default to white")
row = layout.row()
row.template_list("MATERIAL_UL_radiance_materials", "", props, "materials", props, "active_material_index")
if len(props.materials) > 0:
col = layout.column(align=True)
col.prop(props, "category")
if props.category:
col.prop(props, "subcategory")
if props.active_material_index >= 0 and props.active_material_index < len(props.materials):
active_material = props.materials[props.active_material_index]
if active_material.category and active_material.subcategory:
layout.label(
text=f"Mapped: {active_material.name} to {active_material.category} - {active_material.subcategory}"
)
else:
layout.label(text=f"Select category and subcategory for: {active_material.name}")
row = layout.row()
row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials")
row = layout.row()
row.operator("radiance.import_material_mappings", text="Import Mappings", icon="IMPORT")
row.operator("radiance.export_material_mappings", text="Export Mappings", icon="EXPORT")
row = layout.row()
row.operator("bim.refresh_ifc_materials", text="Refresh IFC Materials")
layout.separator()
row = layout.row()
row.label(text="Resolution")
row.prop(props, "radiance_resolution_x", text="X")
row = layout.row()
row.label(text="")
layout.label(text="Step 1: Export geometry for simulation")
row = layout.row()
row.operator("export_scene.radiance", text="Export Geometry for Simulation")
layout.separator()
box = layout.box()
box.label(text="Camera Settings")
row = box.row()
row.prop(props, "use_active_camera")
if not props.use_active_camera:
row = box.row()
row.prop(props, "selected_camera")
row.operator("radiance.select_camera", text="", icon="EYEDROPPER")
row = box.row(align=True)
row.label(text="Resolution")
row.prop(props, "radiance_resolution_x", text="X")
row.prop(props, "radiance_resolution_y", text="Y")
row = layout.row()
@@ -115,8 +124,7 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
row.prop(props, "choose_hdr_image")
row = layout.row()
row.operator("export_scene.radiance", text="Export Geometry for Simulation")
layout.label(text="Step 2: Run the simulation")
row = layout.row()
row.operator("render_scene.radiance", text="Radiance Render")
row.enabled = not props.is_exporting
+50 -50
View File
@@ -375,9 +375,8 @@ class PolylineDecorator:
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
last_point_data = polyline_data[len(polyline_data) - 1]
except:
last_point_data = None
second_to_last_point_data = None
default_container_elevation = 0
last_point_data = None
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
@@ -387,9 +386,14 @@ class PolylineDecorator:
last_point = Vector((0, 0, 0))
if is_input_on:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
)
if cls.use_default_container:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
)
else:
snap_vector = Vector(
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"]))
)
else:
if cls.use_default_container:
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
@@ -405,18 +409,22 @@ class PolylineDecorator:
else:
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x - 1000, last_point.y, last_point.z))
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
distance = (snap_vector - last_point).length
if distance > 0:
angle = tool.Cad.angle_3_vectors(snap_vector, last_point, second_to_last_point, degrees=True)
angle = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True)
# Round angle to the nearest 0.05
angle = round(angle / 0.05) * 0.05
if cls.input_panel:
cls.input_panel["X"] = str(round(snap_vector.x, 4))
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
cls.input_panel["X"] = str(round(snap_vector.x, 3))
cls.input_panel["Y"] = str(round(snap_vector.y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(snap_vector.z, 4))
cls.input_panel["D"] = str(round(distance, 4))
cls.input_panel["A"] = str(round(angle, 4))
cls.input_panel["Z"] = str(round(snap_vector.z, 3))
cls.input_panel["D"] = str(round(distance, 3))
cls.input_panel["A"] = str(round(angle, 3))
return cls.input_panel
@@ -470,13 +478,13 @@ class PolylineDecorator:
try:
polyline_data = context.scene.BIMModelProperties.polyline_point
last_point_data = polyline_data[len(polyline_data) - 1]
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
except:
return
last_point = Vector((0, 0, 0))
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
second_to_last_point = None
if len(polyline_data) > 1:
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
second_to_last_point = Vector(
@@ -485,35 +493,33 @@ class PolylineDecorator:
else:
# Creates a fake "second to last" point away from the first point but in the same x axis
# this allows to calculate the angle relative to x axis when there is only one point
second_to_last_point = Vector((last_point.x - 10, last_point.y, last_point.z))
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
distance = float(cls.input_panel["D"])
if distance < 0 or distance > 0:
angle_rad = radians(180 - float(cls.input_panel["A"]))
ref_vec = second_to_last_point - last_point
dir_vec = last_point - snap_vector
angle = radians(float(cls.input_panel["A"]))
rot_axis = ref_vec.cross(dir_vec)
rot_axis.normalize()
rot_axis = Vector((abs(rot_axis.x), abs(rot_axis.y), abs(rot_axis.z)))
rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
rot_mat = Matrix.Rotation(angle_rad, 3, rot_axis)
ref_vec.normalize()
coords = ((ref_vec @ rot_mat) * distance) + last_point
coords = rot_vector * distance + last_point
x = coords[0]
y = coords[1]
z = coords[2]
if cls.input_panel:
cls.input_panel["X"] = str(round(x, 4))
cls.input_panel["Y"] = str(round(y, 4))
cls.input_panel["X"] = str(round(x, 3))
cls.input_panel["Y"] = str(round(y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(z, 4))
cls.input_panel["Z"] = str(round(z, 3))
return cls.input_panel
cls.input_panel["X"] = str(round(last_point.x, 3))
cls.input_panel["Y"] = str(round(last_point.y, 3))
if "Z" in list(cls.input_panel.keys()):
cls.input_panel["Z"] = str(round(last_point.z, 3))
return cls.input_panel
def draw_batch(self, shader_type, content_pos, color, indices=None):
@@ -522,9 +528,8 @@ class PolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_input_panel(self, context):
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
@classmethod
def format_input_panel_units(cls, context, value):
unit_system = tool.Drawing.get_unit_system()
if unit_system == "IMPERIAL":
precision = context.scene.DocProperties.imperial_precision
@@ -532,6 +537,15 @@ class PolylineDecorator:
else:
precision = None
factor = 1
if context.scene.unit_settings.length_unit == "MILLIMETERS":
factor = 1000
return format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
def draw_input_panel(self, context):
texts = {"D": "Distance: ", "A": "Angle: ", "X": "X coord: ", "Y": "Y coord: ", "Z": "Z coord:", "AREA": "Area: "}
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
@@ -546,11 +560,7 @@ class PolylineDecorator:
if key != "A" and key != self.input_type:
value = float(value)
if context.scene.unit_settings.length_unit == "MILLIMETERS":
value = value * 1000
formatted_value = format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
formatted_value = self.format_input_panel_units(context, value)
else:
formatted_value = value
@@ -582,25 +592,15 @@ class PolylineDecorator:
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
unit_system = tool.Drawing.get_unit_system()
if unit_system == "IMPERIAL":
precision = context.scene.DocProperties.imperial_precision
factor = 3.28084
else:
precision = None
factor = 1
value = measurement_prop[i].dim
value = float(value)
if context.scene.unit_settings.length_unit == "MILLIMETERS":
value = value * 1000
formatted_value = format_distance(
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
)
formatted_value = self.format_input_panel_units(context, value)
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
if i == 1:
continue
pos_angle = measurement_prop[i - 1].position
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
+5 -5
View File
@@ -40,7 +40,8 @@ import json
import collections
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
def update_door_modifier_representation(context: bpy.types.Context) -> None:
obj = context.active_object
props = obj.BIMDoorProperties
element = tool.Ifc.get_entity(obj)
ifc_file = tool.Ifc.get()
@@ -534,10 +535,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_door"
bl_label = "Add Door"
bl_options = {"REGISTER"}
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMDoorProperties
@@ -558,7 +558,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
)
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
return {"FINISHED"}
@@ -612,7 +612,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_door_modifier_representation(context, obj)
update_door_modifier_representation(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
+17 -2
View File
@@ -429,7 +429,15 @@ class DrawPolylineWall(bpy.types.Operator):
index = self.input_options.index(self.input_type)
size = len(self.input_options)
self.input_type = self.input_options[((index + 1) % size)]
self.number_input = []
self.number_input = self.input_panel[self.input_type]
self.number_input = list(self.number_input)
self.number_output = "".join(self.number_input)
if self.input_type != "A":
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
@@ -437,7 +445,13 @@ class DrawPolylineWall(bpy.types.Operator):
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
self.number_input = self.input_panel[self.input_type]
self.number_input = list(self.number_input)
self.number_output = "".join(self.number_input)
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
@@ -507,6 +521,7 @@ class DrawPolylineWall(bpy.types.Operator):
if self.is_input_on:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.recalculate_inputs(context)
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
+5 -5
View File
@@ -36,7 +36,8 @@ from bmesh.types import BMVert
from mathutils import Vector
def update_window_modifier_representation(context, obj):
def update_window_modifier_representation(context):
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
ifc_file = tool.Ifc.get()
@@ -427,10 +428,9 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_window"
bl_label = "Add Window"
bl_options = {"REGISTER"}
obj: bpy.props.StringProperty()
def _execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
obj = context.active_object
element = tool.Ifc.get_entity(obj)
props = obj.BIMWindowProperties
@@ -450,7 +450,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
pset=pset,
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
)
update_window_modifier_representation(context, obj)
update_window_modifier_representation(context)
return {"FINISHED"}
@@ -502,7 +502,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
props.is_editing = False
update_window_modifier_representation(context, obj)
update_window_modifier_representation(context)
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
@@ -200,10 +200,8 @@ class RefreshLibrary(bpy.types.Operator):
self.props.active_library_element = ""
types = IfcStore.library_file.wrapped_data.types_with_super()
for importable_type in sorted(tool.Project.get_appendable_asset_types()):
if importable_type in types:
if IfcStore.library_file.by_type(importable_type):
new = self.props.library_elements.add()
new.name = importable_type
return {"FINISHED"}
@@ -1714,7 +1712,7 @@ class LoadLinkedProject(bpy.types.Operator):
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
if material_ids.size > 0:
if material_ids.size > 0 and len(mesh.polygons) == len(material_ids):
mesh.polygons.foreach_set("material_index", material_ids)
mesh.update()
@@ -2329,7 +2327,7 @@ class MeasureTool(bpy.types.Operator):
self.number_is_negative = False
self.is_input_on = False
self.input_options = ["D", "A", "X", "Y", "Z"]
self.input_type = "OFF"
self.input_type = None
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None
@@ -2355,9 +2353,9 @@ class MeasureTool(bpy.types.Operator):
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
elif self.input_type in {"D", "A"}:
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
self.input_panel[self.input_type] = self.number_output
# self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
else:
self.input_panel[self.input_type] = self.number_output
tool.Blender.update_viewport()
return is_valid
@@ -2367,7 +2365,7 @@ class MeasureTool(bpy.types.Operator):
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
self.mousemove_count += 1
self.is_input_on = False
self.input_type = "OFF"
self.input_type = None
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
@@ -2384,7 +2382,6 @@ class MeasureTool(bpy.types.Operator):
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
return {"RUNNING_MODAL"}
@@ -2418,7 +2415,15 @@ class MeasureTool(bpy.types.Operator):
index = self.input_options.index(self.input_type)
size = len(self.input_options)
self.input_type = self.input_options[((index + 1) % size)]
self.number_input = []
self.number_input = self.input_panel[self.input_type]
self.number_input = list(self.number_input)
self.number_output = "".join(self.number_input)
if self.input_type != "A":
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
@@ -2426,7 +2431,13 @@ class MeasureTool(bpy.types.Operator):
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = "D"
self.number_input = []
self.number_input = self.input_panel[self.input_type]
self.number_input = list(self.number_input)
self.number_output = "".join(self.number_input)
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
self.input_panel[self.input_type] = self.number_output
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
@@ -2438,11 +2449,12 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type in {"D", "A"} and not event.shift:
if event.value == "RELEASE" and event.type in {"D", "A"}:
self.recalculate_inputs(context)
self.is_input_on = True
self.input_type = event.type
self.number_input = []
self.input_panel[self.input_type] = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
@@ -2466,12 +2478,18 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
is_valid = self.recalculate_inputs(context)
if is_valid:
tool.Snap.insert_polyline_point(self.input_panel)
self.is_input_on = False
self.input_type = "OFF"
self.input_type = None
self.number_input = []
self.number_output = ""
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -2509,8 +2527,9 @@ class MeasureTool(bpy.types.Operator):
if self.is_input_on:
if event.value == "RELEASE" and event.type in {"ESC"}:
self.recalculate_inputs(context)
self.is_input_on = False
self.input_type = "OFF"
self.input_type = None
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
else:
@@ -143,6 +143,15 @@ class BIMProjectProperties(PropertyGroup):
active_filter_category_index: IntProperty(name="Active Filter Category Index")
filter_query: StringProperty(name="Filter Query")
should_filter_spatial_elements: BoolProperty(name="Filter Spatial Elements", default=False)
geometry_library: bpy.props.EnumProperty(
items=[
("opencascade", "OpenCASCADE", "Best for stability and accuracy"),
("cgal", "CGAL", "Best for speed"),
("cgal-simple", "CGAL Simple", "CGAL without booleans"),
("hybrid-cgal-simple-opencascade", "Hybrid CGAL-OCC", "First CGAL then fallback to OCC"),
],
name="Geometry Library",
)
should_use_cpu_multiprocessing: BoolProperty(name="CPU Multiprocessing", default=True)
should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
@@ -175,6 +175,8 @@ class BIM_PT_project(Panel):
row = self.layout.row()
row.prop(pprops, "should_merge_materials_by_colour")
row = self.layout.row()
row.prop(pprops, "geometry_library")
row = self.layout.row()
row.prop(pprops, "deflection_tolerance")
row = self.layout.row()
row.prop(pprops, "angular_tolerance")
@@ -78,14 +78,18 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Assign Container"
bl_description = "Assign current default container to the selected objects"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
def _execute(self, context):
props = context.active_object.BIMObjectSpatialProperties
if (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
for element_obj in context.selected_objects:
core.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
)
if self.container:
container = tool.Ifc.get().by_id(self.container)
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
pass
for element_obj in context.selected_objects:
core.assign_container(
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj
)
class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator):
@@ -247,10 +251,17 @@ class ToggleContainerElement(bpy.types.Operator):
bl_idname = "bim.toggle_container_element"
bl_label = "Toggle Container Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle children\nALT+CLICK to recursively toggle children"
element_index: bpy.props.IntProperty()
is_recursive: bpy.props.BoolProperty(name="Is Recursive", default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.is_recursive = True
return self.execute(context)
def execute(self, context):
core.toggle_container_element(tool.Spatial, element_index=self.element_index)
core.toggle_container_element(tool.Spatial, element_index=self.element_index, is_recursive=self.is_recursive)
return {"FINISHED"}
+4 -2
View File
@@ -159,16 +159,18 @@ class BIM_PT_spatial_decomposition(Panel):
if not self.props.total_elements:
row = self.layout.row(align=True)
row.label(text=f"{self.props.active_container.ifc_class} > No Contained Elements", icon="FILE_3D")
row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D")
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
return
row = self.layout.row(align=True)
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Contained Elements",
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
op.container = ifc_definition_id
+10 -10
View File
@@ -414,8 +414,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_window(obj=obj.name)
with context.temp_override(active_object=obj):
bpy.ops.bim.add_window()
elif template == "DOOR":
mesh = bpy.data.meshes.new(name)
@@ -429,8 +429,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_door(obj=obj.name)
with context.temp_override(active_object=obj):
bpy.ops.bim.add_door()
elif template == "STAIR":
mesh = bpy.data.meshes.new(name)
@@ -444,8 +444,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
ifc_class=ifc_class,
should_add_representation=False,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_stair()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_stair()
elif template == "RAILING":
mesh = bpy.data.meshes.new(name)
@@ -460,8 +460,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_railing()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_railing()
elif template == "ROOF":
mesh = bpy.data.meshes.new(name)
@@ -476,8 +476,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
should_add_representation=True,
context=body,
)
tool.Blender.select_and_activate_single_object(context, obj)
bpy.ops.bim.add_roof()
with context.temp_override(active_object=obj):
bpy.ops.bim.add_roof()
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
props.type_class = props.type_class
+6 -1
View File
@@ -41,8 +41,8 @@ def disable_editing_cost_schedule(cost: tool.Cost) -> None:
def remove_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
cost.remove_stored_schedule_columns(cost_schedule)
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
@@ -398,3 +398,8 @@ def add_currency(ifc: tool.Ifc, cost: tool.Cost) -> ifcopenshell.entity_instance
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
ifc.run("unit.assign_unit", units=[unit])
return unit
def generate_cost_schedule_browser(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> bpy.types.Panel:
cost_schedule_data = cost.create_cost_schedule_json(cost_schedule)
return cost.generate_cost_schedule_browser(cost_schedule_data)
+2 -2
View File
@@ -141,8 +141,8 @@ def delete_container(
spatial.import_spatial_decomposition()
def toggle_container_element(spatial: tool.Spatial, element_index: int) -> None:
spatial.toggle_container_element(element_index)
def toggle_container_element(spatial: tool.Spatial, element_index: int, is_recursive: bool) -> None:
spatial.toggle_container_element(element_index, is_recursive=is_recursive)
spatial.load_contained_elements()
+22 -17
View File
@@ -85,34 +85,39 @@ class Cad:
return math.degrees(a) if degrees else a
@classmethod
def angle_3_vectors(cls, v1, v2, v3, degrees=False):
def angle_3_vectors(cls, v1, v2, v3, new_angle=None, degrees=False):
"""
> takes 3 vectors. The order matters, v2 is the center point.
< returns the potentially signed angle as degrees or radians
< returns the signed angle as degrees or radians
< if a new angle is provided, return the rotation vector
"""
d1 = v1 - v2
d2 = v2 - v3
d2 = v3 - v2
axis = d1.cross(d2)
axis.normalize()
axis = Vector((abs(axis.x), abs(axis.y), abs(axis.z)))
d1.normalize()
d2.normalize()
rotation_axis = d1.cross(d2)
axis = d1.cross(d2).normalized()
# Calculate the unsigned angle between the "from" and "to" vectors
# Calculate the unsigned angle between the "d1" and "d2" vectors
a = d1.angle(d2)
# Determine the sign of the angle based on the provided axis
if degrees:
a = math.degrees(a)
parameter = rotation_axis.dot(axis)
sign = 1 if parameter <= 0 else -1
return a * sign
# If new_angle, determine the direction of the rotation
parameter = round(axis.z, 2) < 0 or (round(axis.y, 2) == 0 and round(axis.x < 0)) or (round(axis.x, 2) == 0 and round(axis.y < 0))
if new_angle:
rot_mat = Matrix.Rotation(new_angle, 3, axis)
rot_vector = (d1 @ rot_mat) if parameter else (rot_mat @ d1)
return rot_vector
else:
return a
sign = -1 if parameter else 1
if degrees:
a = math.degrees(a)
return a * sign
else:
return a
@classmethod
def is_x(cls, value: float, x: float, tolerance: float | None = None) -> bool:
+15 -14
View File
@@ -79,26 +79,19 @@ class Collector(bonsai.core.tool.Collector):
cls.link_to_collection_safe(obj, collection)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_to_collection_safe(collection, project_obj.BIMObjectProperties.collection)
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_to_collection_safe(obj, collection)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_to_collection_safe(collection, project_obj.BIMObjectProperties.collection)
elif element.is_a("IfcAnnotation") and (drawing_obj := cls.get_annotation_drawing_obj(element)):
cls.link_to_collection_safe(obj, drawing_obj.BIMObjectProperties.collection)
elif container := ifcopenshell.util.element.get_container(element):
container_obj = tool.Ifc.get_object(container)
if not (collection := container_obj.BIMObjectProperties.collection):
cls.assign(container_obj)
collection = container_obj.BIMObjectProperties.collection
cls.link_to_collection_safe(obj, collection)
elif element.is_a("IfcAnnotation"):
if element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj):
cls.link_to_collection_safe(obj, collection)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
cls.link_to_collection_safe(collection, project_obj.BIMObjectProperties.collection)
else:
for rel in element.HasAssignments or []:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
drawing_obj = tool.Ifc.get_object(related_object)
if drawing_obj:
cls.link_to_collection_safe(obj, drawing_obj.BIMObjectProperties.collection)
else:
collection = cls._create_project_child_collection("Unsorted")
collection.hide_viewport = False
@@ -126,6 +119,14 @@ class Collector(bonsai.core.tool.Collector):
collection.BIMCollectionProperties.obj = obj
return collection
@classmethod
def get_annotation_drawing_obj(cls, element: ifcopenshell.entity_instance) -> bpy.types.Object | None:
for rel in element.HasAssignments or []:
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
return tool.Ifc.get_object(related_object)
@classmethod
def link_to_collection_safe(
cls, obj_or_col: Union[bpy.types.Object, bpy.types.Collection], collection: bpy.types.Collection
+8 -3
View File
@@ -811,9 +811,8 @@ class Cost(bonsai.core.tool.Cost):
@classmethod
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
from bonsai.bim.module.cost.data import CostSchedulesData
if not CostSchedulesData.is_loaded:
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
CostSchedulesData.load()
cost_items = CostSchedulesData.data["cost_items"]
data = []
for rel in cost_schedule.Controls or []:
for cost_item in rel.RelatedObjects or []:
@@ -852,3 +851,9 @@ class Cost(bonsai.core.tool.Cost):
unit = tool.Unit.get_project_currency_unit()
if unit:
return {"id": unit.id(), "name": unit.Currency}
@classmethod
def generate_cost_schedule_browser(cls, cost_schedule_data: list[dict[str, Any]]) -> None:
if not bpy.context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="costing")
tool.Web.send_webui_data(data=cost_schedule_data, data_key="cost_items", event="cost_items")
+3 -2
View File
@@ -1875,7 +1875,7 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod
def get_elements_in_camera_view(
cls, camera: bpy.types.Object, objs: list[ifcopenshell.entity_instance]
cls, camera: bpy.types.Object, objs: list[bpy.types.Object]
) -> set[ifcopenshell.entity_instance]:
props = camera.data.BIMCameraProperties
x = props.width
@@ -1886,7 +1886,8 @@ class Drawing(bonsai.core.tool.Drawing):
[
tool.Ifc.get_entity(o)
for o in objs
if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
if o
and cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
and tool.Ifc.get_entity(o)
]
)
+1
View File
@@ -582,6 +582,7 @@ class Geometry(bonsai.core.tool.Geometry):
settings = ifcopenshell.geom.settings()
settings.set("weld-vertices", True)
settings.set("apply-default-materials", False)
settings.set("layerset-first", True)
settings.set("keep-bounding-boxes", True)
context = representation.ContextOfItems
+1
View File
@@ -568,6 +568,7 @@ class Loader(bonsai.core.tool.Loader):
settings.set("context-ids", [context.id()])
settings.set("apply-default-materials", False)
settings.set("keep-bounding-boxes", True)
settings.set("layerset-first", True)
if is_gross:
settings.set("disable-opening-subtractions", True)
results.append(settings)
+7 -4
View File
@@ -524,14 +524,14 @@ class Snap(bonsai.core.tool.Snap):
def validate_input(cls, input_number, input_type):
grammar_imperial = """
start: FORMULA? dim expr?
start: (FORMULA dim expr) | dim
dim: imperial
FORMULA: "="
imperial: feet? "-"? inches?
feet: NUMBER? " "? fraction? "'"
inches: NUMBER? " "? fraction? "\\""
feet: NUMBER? "-"? fraction? "'"
inches: NUMBER? "-"? fraction? "\\""
fraction: NUMBER "/" NUMBER
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
@@ -583,7 +583,10 @@ class Snap(bonsai.core.tool.Snap):
def imperial(self, args):
if len(args) > 1:
result = args[0] + args[1]
if args[0] <= 0:
result = args[0] - args[1]
else:
result = args[0] + args[1]
else:
result = args[0]
return result
+49 -13
View File
@@ -199,14 +199,8 @@ class Spatial(bonsai.core.tool.Spatial):
src_obj.location.xy = destination_obj.location.xy
@classmethod
def load_contained_elements(cls) -> None:
def get_grouped_elements_in_container(cls, container: ifcopenshell.entity_instance) -> dict:
props = bpy.context.scene.BIMSpatialDecompositionProperties
props.elements.clear()
if not (container := props.active_container):
return
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
@@ -222,25 +216,39 @@ class Spatial(bonsai.core.tool.Spatial):
class_data = results.setdefault(ifc_class, {})
type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []})
type_data["elements"].append(element)
return results
@classmethod
def load_contained_elements(cls) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
props.elements.clear()
if not (container := props.active_container):
return
container = tool.Ifc.get().by_id(container.ifc_definition_id)
results = cls.get_grouped_elements_in_container(container)
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", [])
expanded_classes_r = expanded_elements.get("CLASS_R", [])
total_elements = 0
for ifc_class in sorted(results.keys()):
new = props.elements.add()
new.name = ifc_class
new.type = "CLASS"
class_is_expanded = ifc_class in expanded_classes
new.is_expanded = class_is_expanded
class_is_expanded_r = ifc_class in expanded_classes_r
new.is_expanded = class_is_expanded or class_is_expanded_r
total = 0
for ifc_definition_id in sorted(
results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"]
):
type_data = results[ifc_class][ifc_definition_id]
total2 = len(type_data["elements"])
if class_is_expanded:
if new.is_expanded:
new2 = props.elements.add()
new2.type = "TYPE"
new2.name = type_data["type_name"]
@@ -251,9 +259,9 @@ class Spatial(bonsai.core.tool.Spatial):
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
new2.is_expanded = type_is_expanded or class_is_expanded_r
if type_is_expanded:
if new2.is_expanded:
for element in type_data["elements"]:
occurrence = props.elements.add()
occurrence.name = element.Name or "Unnamed"
@@ -363,25 +371,53 @@ class Spatial(bonsai.core.tool.Spatial):
props.contracted_containers = json.dumps(contracted_containers)
@classmethod
def toggle_container_element(cls, element_index: int) -> None:
def toggle_container_element(cls, element_index: int, is_recursive: bool) -> 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:
elif element.type == "TYPE":
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
else:
return
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)
should_expand = False
else:
expanded_elements_list.append(filtered_item)
should_expand = True
if is_recursive and element.type == "CLASS":
container = tool.Ifc.get().by_id(props.active_container.ifc_definition_id)
results = cls.get_grouped_elements_in_container(container)
for ifc_class in results.keys():
if ifc_class != element.name:
continue
for ifc_definition_id in results[ifc_class].keys():
if ifc_definition_id == 0:
element_type = "UNTYPED_CLASSES"
filtered_item = ifc_class
else:
element_type = "IFC_ID"
filtered_item = ifc_definition_id
expanded_elements_list = expanded_elements.setdefault(element_type, [])
if should_expand is False and filtered_item in expanded_elements_list:
expanded_elements_list.remove(filtered_item)
elif should_expand is True and filtered_item not in expanded_elements_list:
expanded_elements_list.append(filtered_item)
props.expanded_elements = json.dumps(expanded_elements)
# HERE STARTS SPATIAL TOOL
+65 -7
View File
@@ -21,6 +21,7 @@ from bonsai.bim.module.web.data import WebData
import bonsai.core.tool
import bonsai.tool as tool
import ifcopenshell.api.sequence
import ifcopenshell.api.cost
from typing import Any, Dict, Optional
import time
import socket
@@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "loadCostSchedule":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
if operator_data["type"] == "addCostItem":
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "selectAssignedElements":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=True)
if operator_data["type"] == "addCostItem":
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "editCostItemName":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
tool.Ifc.run(
@@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web):
attributes = {"Name": operator_data["name"]}
)
tool.Cost.load_cost_schedule_tree()
if operator_data["type"] == "enableEditingCostValues":
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
cost_values = ifcopenshell.util.cost.get_cost_values(cost_item)
cls.send_webui_data(data={
"cost_values": cost_values,
"cost_item_id": operator_data["costItemId"]
}, data_key="cost_values", event="cost_values")
if operator_data["type"] == "addCostValue":
value = ifcopenshell.api.cost.add_cost_value(
ifc_file,
parent=ifc_file.by_id(operator_data["costItemId"]),
)
cls.send_webui_data(
data={
"cost_value_id" : value.id(),
"cost_item_id":operator_data["costItemId"]},
data_key="cost_value",
event="cost_value"
)
if operator_data["type"] == "editCostValues":
cost_item_id = operator_data["costItemId"]
print('Editing cost values', cost_item_id)
print(cost_item_id)
print(type(operator_data["costValues"]))
for value_data in operator_data["costValues"] or []:
print(value_data)
value = ifc_file.by_id(value_data["id"])
print(value.get_info())
if value_data["costType"] == "FIXED":
attributes= {
"AppliedValue": value_data["appliedValue"],
}
elif value_data["costType"] == "CATEGORY":
attributes= {
"AppliedValue": value_data["appliedValue"],
"Category": value_data["costCategory"],
}
elif value_data["costType"] == "SUM":
attributes= {
"Category": '*'
}
ifcopenshell.api.cost.edit_cost_value(
file=ifc_file,
cost_value=value,
attributes= attributes
)
tool.Cost.load_cost_schedule_tree()
cost_item = ifc_file.by_id(operator_data["costItemId"])
if not cost_item:
print("Cost item not found")
return
cost_schedule = tool.Cost.get_cost_schedule(cost_item=cost_item)
cls.load_cost_schedule_web_ui(cost_schedule)
@classmethod
def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
@classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None:
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -9,6 +9,7 @@ This chapter covers how you can help contribute to Bonsai.
:hidden:
:maxdepth: 2
installation
hello_world
running_tests
translations
@@ -3,20 +3,16 @@ Installation
There are different methods of installation, depending on your situation.
1. **Unstable installation** is recommended for power users helping with testing.
2. **Bundling for Blender** is recommended for distributing the add-on.
3. **Live development environment** is recommended for developers who are actively coding.
4. **Packaged installation** is recommended for those who use a package manager.
1. :ref:`guides/development/installation:Unstable installation` is recommended
for power users helping with testing.
2. :ref:`guides/development/installation:Bundling for blender` is recommended for distributing the add-on.
3. :ref:`guides/development/installation:Live development environment` is
recommended for developers who are actively coding.
4. :ref:`guides/development/installation:Packaged installation` is recommended
for those who use a package manager.
Unstable installation
---------------------
**Unstable installation** is almost the same as **Stable installation**, except
that they are typically updated every day. Simply download a daily build from
the `GitHub releases page
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
then follow the usual :doc:`installation
instructions</users/quickstart/installation>`.
System requirements
-------------------
Bonsai officially supports all major 64-bit platforms, as well as the Python
version shipped by the Blender Foundation for the most recent three major
@@ -41,6 +37,76 @@ Other system specifications match the `Blender Requirements
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
but it happens.
Unstable installation
---------------------
**Unstable installation** is almost the same as **Stable installation**, except
that they are typically updated every day. To install the **Unstable** version:
1. Open up Blender, and click on ``Edit > Preferences``.
.. image:: /quickstart/images/install-bonsai-1.png
2. Select the **Get Extensions** tab, and press **Allow Online Access**.
.. image:: /quickstart/images/install-bonsai-2.png
3. Go to the `Bonsai Unstable Repository
<https://github.com/IfcOpenShell/bonsai_unstable_repo>`__, and drag and drop
from the appropriate link in the ``ID`` column of the table into Blender
depending on your operating system.
.. image:: images/unstable-drag-drop.png
4. Enable **Check for Updates on Startup** to get updates for daily Bonsai
builds automatically.
.. image:: images/unstable-auto-update.png
.. tip::
Instead of drag and drop, you can manually create the repository:
Open :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
--> Repositories (Top Right) --> "+" Icon --> Add Remote Remository`.
You'll see a window similar to the one above.
Use as URL:
``https://raw.githubusercontent.com/IfcOpenShell/bonsai_unstable_repo/main/index.json``
and enable **Check for Updates on Startup** if you want them.
5. Search for **Bonsai** in the top left search bar, then press the **Install**
button.
.. image:: /quickstart/images/install-bonsai-3.png
.. warning::
Make sure the extension you install has ``raw.githubusercontent.com`` as
it's "Repository" (not ``extensions.blender.org``).
.. image:: images/unstable-repo.png
6. Whenever a new update is available, you'll see it in the bottom right
:menuselection:`Status Bar`
.. image:: images/unstable-icon.png
7. To update, click on the update button in :menuselection:`Topbar --> Edit -->
Preferences --> Get Extensions`.
.. image:: /guides/images/update.png
8. After an update, be sure to restart.
.. image:: images/unstable-restart.png
If you wish to install an **Unstable** version offline, you can download a
daily build from the `GitHub releases page
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
then go to :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
--> "V" Icon (top right) --> Install from Disk`.
Bundling for Blender
--------------------
@@ -72,12 +138,13 @@ Live development environment
----------------------------
One option for developers who want to actively develop from source is to follow
the instructions from :ref:`devs/installation:Bundling for Blender`. However,
the instructions from :ref:`guides/development/installation:Bundling for Blender`. However,
creating a build, uninstalling the old add-on, and installing a new build is a
slow process. Although it works, it is very slow, so we do not recommend it.
A more rapid approach is to follow the :ref:`devs/installation:Unstable
installation` method, as this provides all dependencies for you out of the box.
A more rapid approach is to follow the
:ref:`guides/development/installation:Unstable installation` method, as this
provides all dependencies for you out of the box.
Once you've done this, you can replace certain Python files that tend to be
updated frequently with those from the Git repository. We're going to use
+4 -4
View File
@@ -30,13 +30,13 @@ Updating
We always recommend to use the latest version.
Open up Blender, click on ``Edit > Preferences``, and select the **Get
Extensions** tab. If an update is available, you will see a button next to the
Open up Blender, click on :menuselection:`Topbar --> Edit --> Preferences -->
Get Extensions`. If an update is available, you will see a button next to the
**Bonsai** add-on.
Updates are typically available every 2 months. If you need something more
frequent, check out :ref:`devs/installation:unstable installation` which is
updated every day.
frequent, check out :ref:`guides/development/installation:Unstable
installation` which is updated every day.
.. image:: images/update.png
+79 -86
View File
@@ -121,49 +121,34 @@ pressing the button on the top right of the **Viewport** panel. The hotkey is
Overview of all objects
-----------------------
The **Outliner** panel on the top right shows a hierarchy of all the currently
loaded physical **Objects** in your IFC project. These **Objects** correlate to
what you can see in the **Viewport** panel.
All physical **Elements** are organised in a hierarchy of **Spatial
Containers**. By default, this hierarchy represents a breakdown of spaces, from
large spaces such as a site and a building, down to smaller spaces like
building storeys and room spaces. The hierarchy will always begin with an
**IfcProject**.
Every **Object** in the **Outliner** represents an IFC **Element**. These
**Objects** have a name with the pattern ``Class/Name``. The class prefix
represents the type of object, and the name is the name of the object. Examples
of classes you will see are ``IfcBuilding``, or ``IfcWall``. This naming
convention makes it easy to quickly spot types of objects.
The :menuselection:`Properties --> Project Overview --> Spatial Decomposition`
panel shows this hierarchy. Below, it shows a list of all **Elements**
contained inside the actively selected **Spatial Container**.
.. image:: images/outliner.png
.. image:: images/spatial-tree.png
Objects are organised in a hierarchy. By default, this hierarchy represents a
breakdown of spaces, from large spaces such as a site and a building, down to
smaller spaces like building storeys and room spaces. The hierarchy will always
begin with an **IfcProject** object. You can click on the triangle to toggle the
hierarchy.
With a **Spatial Container** selected, use the **Isolate** button or **Hide /
Show Icons** to quickly focus or control visibility. Use the search filters at
the bottom of the **Container** or **Element** lists to quickly find objects,
and use the **Select Icon** to select them.
.. image:: images/spatial-tree-features.png
**Elements** are grouped into IFC **Classes**, such as Wall, Slab, or Door.
Within that, **Elements** are grouped into **Construction Types**. You'll see a
count of how many objects of that type exist. In the example above, there is
only one stair contained in the building storey.
.. tip::
In large projects with deep hierarchies, you can ``Shift-LMB`` click the
triangle to recursively toggle the hierarchy. You can also click and drag the
``MMB`` to pan left and right.
When there are lots of objects, you can type a name in the filter box to quickly
identify objects by name or type.
.. image:: images/outliner-filter.png
Clicking on an object in the **Outliner** panel also selects the corresponding
object in the **Viewport** panel. A good strategy to find objects is to then use
``View > Frame Selected`` to zoom to it in the **Viewport**.
The **Outliner** panel is also great for isolating portions of your project. You
can include and exclude portions by clicking on the **Tick Icon** next to
collections of objects in the hierarchy.
Let's isolate a single building storey. Start by disabling the **Tick Icon**
next to the **IfcProject** collection. This will hide everything in the project.
Then navigate through the hierarchy and enable the **Tick Icon** next to an
**IfcBuildingStory**.
.. image:: images/outliner-isolate.png
Hold :kbd:`Alt` when clicking on triangles in the hierarchy to show / hide
children recursively.
Viewing element classes
-----------------------
@@ -185,16 +170,15 @@ properties and relationships it is allowed to have. For example, a Wall
worry about memorising all the available **Classes**, you'll get a feel for them
as you explore more.
To view an object's class, click on an object in the **Viewport** or **Outliner**
panel, then switch to the **Object Information** tab in the **Properties** panel.
You can see the **Class** name in the **Object Metadata** subpanel.
To view an object's class, click on an object in the :menuselection:`3D
Viewport`, then go to :menuselection:`Properties --> Object Information -->
Object Metadata` to see the **Class** name.
.. image:: images/element-class.png
In this case, the **Class** of our roof is an **IfcSlab**. You'll notice this is
the same **Class** name used as a prefix for the object name in the **Outliner**
panel. You can also see the name of the actively selected object in the top left
of the **Viewport** panel.
In this case, the **Class** of our roof is an **IfcSlab**. You can also see the
name of the actively selected object in the top left of the :menuselection:`3D
Viewport`.
.. warning::
@@ -219,24 +203,26 @@ not see it all the time.
classes and predefined types you should see.
Press the **Select Icon** to select all objects that are of the same
**IfcSlab** **Class**. Then, you can isolate these elements by going to ``Object
> Show/Hide > Hide Unselected`` (hotkey ``Shift-H``). To show all elements again, you can use
``Object > Show/Hide > Show Hidden Objects`` (hotkey ``Alt-H``). If you want to
hide elements instead, you can use ``Object > Show/Hidden > Hide Selected``
(hotkey ``H``).
**IfcSlab** **Class**. Then, you can isolate these elements by going to
:menuselection:`3D Viewport --> Object --> Show/Hide --> Hide Unselected`
(:kbd:`Shift-H`). To show all elements again, you can use :menuselection:`3D
Viewport --> Object --> Show/Hide --> Show Hidden Objects` (:kbd:`Alt-H`). If
you want to hide elements instead, you can use :menuselection:`3D Viewport -->
Object --> Show/Hidden --> Hide Selected` (:kbd:`H`).
.. image:: images/element-class-select.png
.. note::
Remember that Blender's hotkeys are context sensitive. Make sure your mouse
is hovering over the **Viewport** panel when you press a hotkey or no cake
for you.
is hovering over the :menuselection:`Viewport` panel when you press a hotkey
or no cake for you.
You can also see statistics about the number of selected objects. If you right
click on the bottom right status bar and enable **Scene Statistics** you will
see information like **Objects 4/4**, which means that 4 objects are selected
out of 4 available objects. This is a great way of counting objects like toilets.
click on the bottom right :menuselection:`Status Bar` and enable **Scene
Statistics** you will see information like **Objects 4/4**, which means that 4
objects are selected out of 4 available objects. This is a great way of
counting objects like toilets.
.. image:: images/scene-statistics.png
@@ -244,7 +230,7 @@ Viewing attributes and properties
---------------------------------
You can view the **Attributes**, **Properties**, and **Quantities** of the
selected object in the **Object Properties** tab.
selected object in the :menuselection:`Properties --> Object Information` tab.
Let's focus on **Attributes** first. Scroll down to the **Attributes**
subpanel. **Attributes** are a limited set of fundamental data (usually less
@@ -279,7 +265,8 @@ have different **Properties** depending on what information they want to store.
Each **Property** has a name and a value, and are grouped into **Property
Sets**. Each **Property Set** also has a name.
You can find **Properties** in the **Object Property Sets** subpanel.
You can find **Properties** in the :menuselection:`Properties --> Object
Information --> Property Sets` panel.
.. image:: images/psets.png
@@ -303,7 +290,8 @@ value, and are grouped into **Quantity Sets**. Similarly, there are also common
quantities defined as part of the international standard, denoted by the prefix
``Qto_``. This prefix is short for "Quantity Take-Off".
You can find **Quantities** in the **Object Quantity Sets** subpanel.
You can find **Quantities** in the :menuselection:`Properties --> Object
Information --> Quantity Sets` panel.
.. image:: images/qtos.png
@@ -313,16 +301,17 @@ Finding the location of objects
Every object in the built environment has a location in the world. For example,
a chair will be located in a space, and a wall is typically located in a
building storey. You've already seen this hierarchy of spaces in the
**Outliner** panel, where an IFC project is broken down into site, building,
storeys, and spaces.
:menuselection:`Properties --> Project Overview --> Spatial Decomposition`
panel, where an IFC project is broken down into site, building, storeys, and
spaces.
Sometimes, objects may have multiple relevant locations, such
as a multi-storey column which can be related to multiple building storeys.
Even in these cases, IFC enforces one location to be its primary
location, known as its **Spatial Container**.
If you click on any object, you can see its location in the **Spatial
Container** subpanel in the **Object Information** tab.
If you click on any object, you can see its location in the
:menuselection:`Properties --> Object Information --> Spatial Container` panel.
Press the **Select Icon** to select all objects that are in the same location.
@@ -334,20 +323,22 @@ Checking construction types
Almost everything in the built environment will have a **Construction Type**.
For example, an architect will specify a door type for every door in a project.
You can see a list of **Construction Types** in the **Outliner** panel in the
**Types** collection. For example, if the architect has a wall types schedule
with the wall type names of ``WT01``, ``WT02``, and ``WT03``, you should see
three **IfcWallType** objects with those same names in the **Outliner**.
You can see a list of **Construction Types** in the :menuselection:`Outliner`
in the **Types** collection. For example, if the architect has a wall types
schedule with the wall type names of ``WT01``, ``WT02``, and ``WT03``, you
should see three **IfcWallType** objects with those same names in the
**Outliner**.
You can click on these types to see more details about them in the
**Properties** panel.
:menuselection:`Properties` panel.
.. image:: images/outliner-types.png
When selecting an object, you can also see its construction type in **Object
Information** under the **Type** subpanel. You can press the **Select Icon** to
select all objects that are of the same **Construction Type**. You can use the
hide and isolate hotkeys to quickly view them in the model.
When selecting an object, you can also see its construction type in
:menuselection:`Properties --> Object Information --> Type`. You can press the
**Select Icon** to select all objects that are of the same **Construction
Type**. You can use the hide and isolate hotkeys to quickly view them in the
model.
.. image:: images/properties-types.png
@@ -361,10 +352,10 @@ geometry of the pump, so all occurrences of that pump will have the same
geometry.
You can visually inspect types in isolation to the rest of the model. Types are
hidden by default, so first enable the visibility of the **Types** collection in
the **Outliner** by pressing the **Visibility Icon**. Then, select a type, and
click on ``View > Local View > Toggle Local View`` (hotkey ``/``) in the
**Viewport**. Toggle the view to see the entire model again.
hidden by default, so first enable the visibility of the **Types** collection
in the **Outliner** by pressing the **Visibility Icon**. Then, select a type,
and click on :menuselection:`3D Viewport --> View --> Local View --> Toggle
Local View` (:kbd:`/`). Toggle the view to see the entire model again.
.. image:: images/type-local-view.png
@@ -383,8 +374,8 @@ resources. For example, a **Material** might be blockwork. Another
**Material** might be in-situ concrete. **Materials** are grouped into
categories like steel, concrete, brick, block, and so on.
We can see a list of **Materials** used in the project in the **Materials**
subpanel in the **Geometry and Materials** tab.
We can see a list of **Materials** used in the project in the
:menuselection:`Properties --> Geometry and Materials --> Materials` panel.
Press the **Select Icon** to select all objects that are of the selected
material.
@@ -395,18 +386,20 @@ Taking simple measurements
--------------------------
The simplest form of measurement is the one that's already taken for you. The
**Viewing attributes and properties** section describes how to view
pre-calculated **Quantities**.
:ref:`quickstart/explore_model:Viewing attributes and properties` section
describes how to view pre-calculated **Quantities**.
Sometimes, you may wish to take manual measurements yourself. You can view the
overall X, Y, and Z dimensions of the currently selected object in the
**Derived Coordinates** subpanel in the **Geometry and Materials** tab.
:menuselection:`Properties --> Geometry and Materials --> Placement --> Derived
Coordinates` panel.
.. image:: images/dimensions.png
Another way to manually measure from two points is to use the **Measure** tool.
First, press the **Snap Icon** to enable snapping. Then choose snap targets in
the **Snap Menu** in the top middle section of the **Viewport** panel.
the **Snap Menu** in the top middle section of the :menuselection:`3D
Viewport`.
.. image:: images/snap-targets.png
@@ -418,12 +411,12 @@ the **Snap Menu** in the top middle section of the **Viewport** panel.
measurements will automatically snap to the nearest object's surface.
Now that you have configured snapping, press the **Measure Tool Icon** on the
left of the **Viewport** panel. **Click** and **Drag** in the 3D viewport to
take a measurement. A circle will appear guiding the first point of your
measurement. While **Dragging**, press the ``X`` key to lock the measurement
line along the X axis. Alternatively, press the ``Y`` or ``Z`` key to lock the
measurement line along the Y or Z axis. Let go of the mouse to finish your
measurement.
left of the :menuselection:`3D Viewport`. **Click** and **Drag** in the 3D
viewport to take a measurement. A circle will appear guiding the first point of
your measurement. While **Dragging**, press the ``X`` key to lock the
measurement line along the X axis. Alternatively, press the ``Y`` or ``Z`` key
to lock the measurement line along the Y or Z axis. Let go of the mouse to
finish your measurement.
.. image:: images/measure-tool.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+2 -2
View File
@@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64)
PLATFORMTAG:=win_amd64
endif
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-7e6607a-$(PLATFORM).zip
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-7e6607a-$(PLATFORM).zip
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-03935a9-$(PLATFORM).zip
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-03935a9-$(PLATFORM).zip
.PHONY: test
test:
@@ -246,6 +246,26 @@ def get_cost_item_assignments(
]
def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, str]]:
results = []
for cost_value in cost_item.CostValues or []:
label = "{0:.2f}".format(calculate_applied_value(cost_item, cost_value))
label += " = {}".format(serialise_cost_value(cost_value))
results.append(
{
"id": cost_value.id(),
"label": label,
"name": cost_value.Name,
"category": cost_value.Category,
"applied_value": (
get_primitive_applied_value(cost_value.AppliedValue) if cost_value.AppliedValue else None
),
}
)
print(results)
return results
class CostValueUnserialiser:
def parse(self, formula: str):
l = lark.Lark(
@@ -0,0 +1,60 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import pytest
import test.bootstrap
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.element
class TestGetInfo2(test.bootstrap.IFC4):
def test_instance_attribute(self):
brep = self.file.create_entity("IfcFacetedBrep")
shell = self.file.create_entity("IfcClosedShell")
brep.Outer = shell
assert brep.get_info_2(recursive=True) == {
"Outer": {"CfsFaces": None, "id": 2, "type": "IfcClosedShell"},
"id": 1,
"type": "IfcFacetedBrep",
}
def test_aggregate_of_instance_attribute(self):
shell = self.file.create_entity("IfcClosedShell")
faces = [self.file.create_entity("IfcFace") for i in range(3)]
shell.CfsFaces = faces
assert shell.get_info_2(recursive=True)["CfsFaces"] == (
{"Bounds": None, "id": 2, "type": "IfcFace"},
{"Bounds": None, "id": 3, "type": "IfcFace"},
{"Bounds": None, "id": 4, "type": "IfcFace"},
)
def test_aggregate_of_aggregate_of_instance_attribute(self):
surface = self.file.create_entity("IfcBSplineSurfaceWithKnots")
pp = [self.file.create_entity("IfcCartesianPoint", [float(i)]) for i in range(4)]
surface.ControlPointsList = [pp[:2], pp[2:]]
assert surface.get_info_2(recursive=True)["ControlPointsList"] == (
(
{"Coordinates": (0.0,), "id": 2, "type": "IfcCartesianPoint"},
{"Coordinates": (1.0,), "id": 3, "type": "IfcCartesianPoint"},
),
(
{"Coordinates": (2.0,), "id": 4, "type": "IfcCartesianPoint"},
{"Coordinates": (3.0,), "id": 5, "type": "IfcCartesianPoint"},
),
)
+4
View File
@@ -52,6 +52,10 @@ class IFC_PARSE_API HeaderEntity {
return data_.size();
}
AttributeValue getArgument(size_t index) const {
return data_.get_attribute_value(index);
}
std::string toString(bool upper = false) const {
std::stringstream stream;
stream << datatype_;
@@ -136,21 +136,21 @@ class Patcher:
if material.is_a("IfcMaterial"):
materials = []
elif material.is_a("IfcMaterialLayerSet"):
for idx, item in enumerate(material.MaterialLayers):
for idx, item in enumerate(material.MaterialLayers or []):
material = item.Material
properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)])
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
if category := getattr(material, "Category", None):
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
elif material.is_a("IfcMaterialProfileSet"):
for idx, item in enumerate(material.MaterialProfiles):
for idx, item in enumerate(material.MaterialProfiles or []):
material = item.Material
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
if category := getattr(material, "Category", None):
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
elif material.is_a("IfcMaterialConstituentSet"):
for idx, item in enumerate(material.MaterialConstituents):
for idx, item in enumerate(material.MaterialConstituents or []):
material = item.Material
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name])
+39
View File
@@ -520,6 +520,26 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas
}
}
// Expose FileDescription and FileName header entities
// to make them readable even if they were not filled properly before.
// Though it is invalid IFC, technically.
// FileSchema is not exposed as IFC file won't load if it's invalid.
%extend IfcParse::FileDescription {
AttributeValue description() const { return $self->getArgument(0); }
AttributeValue implementation_level() const { return $self->getArgument(1); }
};
%extend IfcParse::FileName {
AttributeValue name() const { return $self->getArgument(0); }
AttributeValue time_stamp() const { return $self->getArgument(1); }
AttributeValue author() const { return $self->getArgument(2); }
AttributeValue organization() const { return $self->getArgument(3); }
AttributeValue preprocessor_version() const { return $self->getArgument(4); }
AttributeValue originating_system() const { return $self->getArgument(5); }
AttributeValue authorization() const { return $self->getArgument(6); }
};
%extend IfcParse::IfcSpfHeader {
%pythoncode %{
# Hide the getters with read-only property implementations
@@ -773,6 +793,25 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas
Py_INCREF(Py_None);
return static_cast<PyObject*>(Py_None);
}
} else if constexpr (std::is_same_v<U, IfcUtil::IfcBaseClass*>) {
return get_info_cpp(v);
} else if constexpr (std::is_same_v<U, aggregate_of_instance::ptr>) {
auto r = PyTuple_New(v->size());
for (unsigned i = 0; i < v->size(); ++i) {
PyTuple_SetItem(r, i, get_info_cpp((*v)[i]));
}
return r;
} else if constexpr (std::is_same_v<U, aggregate_of_aggregate_of_instance::ptr>) {
auto rs = PyTuple_New(v->size());
for (auto it = v->begin(); it != v->end(); ++it) {
auto v_i = it;
auto r = PyTuple_New(v_i->size());
for (unsigned i = 0; i < v_i->size(); ++i) {
PyTuple_SetItem(r, i, get_info_cpp((*v_i)[i]));
}
PyTuple_SetItem(rs, std::distance(v->begin(), it), r);
}
return rs;
} else if constexpr (std::is_same_v<U, empty_aggregate_t> || std::is_same_v<U, empty_aggregate_of_aggregate_t> || std::is_same_v<U, Blank>) {
Py_INCREF(Py_None);
return static_cast<PyObject*>(Py_None);