Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e072870fd3 | |||
| af4475fe46 | |||
| eb3104367a | |||
| 90f9ebd25e | |||
| f2554bc968 | |||
| 53a78f38e5 | |||
| b476cb825c | |||
| ec21574be5 | |||
| b552665dd9 | |||
| 48b630696f | |||
| 8c94ef6e54 | |||
| bdcadfd100 | |||
| 90e83d77a7 | |||
| 2e80363bac | |||
| b6f80eabb4 | |||
| c93e52089b | |||
| 76ba3947f5 | |||
| 50cb09aac2 | |||
| dac1aaa7f3 | |||
| eed603bbcd | |||
| 1d62f6f736 | |||
| 799cf47316 | |||
| 03935a91da | |||
| 37baa9235a | |||
| b4b2b60935 | |||
| a11c7535d8 | |||
| e8d6dd333c | |||
| 9633d92c17 | |||
| afd5a5b23e | |||
| 15481c0a59 | |||
| 32b37b8823 | |||
| f3ab214752 | |||
| 4a59b30f1b | |||
| 87d6960356 | |||
| aff893e690 | |||
| 486eb259ea | |||
| 8164b496f8 | |||
| fda23f350a | |||
| 711c597065 | |||
| 80a396dc12 | |||
| 2b70f12d44 | |||
| c3bcc2d90c | |||
| 88e5a51f01 |
@@ -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
|
||||
|
||||
@@ -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 | [](https://pypi.org/project/bcf-client/) |
|
||||
| ~~blenderbim~~ Bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=blenderbim&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](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 | [](https://pypi.org/project/ifc4d/) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "×";
|
||||
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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -265,16 +265,10 @@ class CostSchedulesData:
|
||||
|
||||
@classmethod
|
||||
def cost_values(cls):
|
||||
results = []
|
||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||
if not ifc_id:
|
||||
return results
|
||||
cost_item = tool.Ifc.get().by_id(ifc_id)
|
||||
for cost_value in cost_item.CostValues or []:
|
||||
label = "{0:.2f}".format(ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value))
|
||||
label += " = {}".format(ifcopenshell.util.cost.serialise_cost_value(cost_value))
|
||||
results.append({"id": cost_value.id(), "label": label, "name": cost_value.Name})
|
||||
return results
|
||||
return []
|
||||
return ifcopenshell.util.cost.get_cost_values(tool.Ifc.get().by_id(ifc_id))
|
||||
|
||||
@classmethod
|
||||
def quantity_types(cls):
|
||||
|
||||
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
core.add_currency(tool.Ifc, tool.Cost)
|
||||
|
||||
|
||||
class GenerateCostScheduleBrowser(bpy.types.Operator):
|
||||
bl_idname = "bim.generate_cost_schedule_browser"
|
||||
bl_label = "Generate Cost Schedule Browser"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
cost_schedule: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
core.generate_cost_schedule_browser(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule))
|
||||
return {"FINISHED"}
|
||||
@@ -73,7 +73,8 @@ class BIM_PT_cost_schedules(Panel):
|
||||
row1.label(text="Schedule tools")
|
||||
row1 = col.row(align=True)
|
||||
row1.alignment = "RIGHT"
|
||||
row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"]
|
||||
row1.operator("bim.export_cost_schedules", text="Export spreadsheet", icon="EXPORT").cost_schedule = cost_schedule["id"]
|
||||
row1.operator("bim.generate_cost_schedule_browser", text="Generate spreadsheet browsser", icon="URL").cost_schedule = cost_schedule["id"]
|
||||
row2 = col.row(align=True)
|
||||
row2.alignment = "RIGHT"
|
||||
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
|
||||
|
||||
@@ -204,6 +204,8 @@ def format_distance(
|
||||
tx_dist = ""
|
||||
if feet is not None:
|
||||
tx_dist += str(feet) + "'"
|
||||
if not feet and not add_inches:
|
||||
tx_dist += str(feet) + "'"
|
||||
if feet and add_inches:
|
||||
tx_dist += " - "
|
||||
if not feet and value < 0:
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import bpy
|
||||
import json
|
||||
import time
|
||||
@@ -43,13 +42,12 @@ import bonsai.core.drawing as core
|
||||
import bonsai.bim.module.drawing.svgwriter as svgwriter
|
||||
import bonsai.bim.module.drawing.annotation as annotation
|
||||
import bonsai.bim.module.drawing.sheeter as sheeter
|
||||
import bonsai.bim.module.drawing.scheduler as scheduler
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
import bonsai.bim.export_ifc
|
||||
from bonsai.bim.module.drawing.decoration import CutDecorator
|
||||
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||
from typing import NamedTuple, List, Union, Optional, Literal
|
||||
from lxml import etree
|
||||
from math import radians
|
||||
from mathutils import Vector, Color, Matrix
|
||||
from timeit import default_timer as timer
|
||||
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
|
||||
@@ -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"}
|
||||
|
||||
@@ -390,8 +390,31 @@ class DocProperties(PropertyGroup):
|
||||
|
||||
|
||||
class BIMCameraProperties(PropertyGroup):
|
||||
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
|
||||
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
|
||||
linework_mode: EnumProperty(
|
||||
items=[
|
||||
("OPENCASCADE", "OpenCASCADE", "Slower, more accurate, with more features"),
|
||||
("FREESTYLE", "Freestyle", "Faster, less accurate, no fill support"),
|
||||
],
|
||||
default="OPENCASCADE",
|
||||
name="Linework Mode",
|
||||
)
|
||||
fill_mode: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", "Disable filling areas seen in projection"),
|
||||
("SHAPELY", "Shapely", "Recommended"),
|
||||
("SVGFILL", "SVGFill", "Experimental"),
|
||||
],
|
||||
default="NONE",
|
||||
name="Fill Mode",
|
||||
)
|
||||
cut_mode: EnumProperty(
|
||||
items=[
|
||||
("BISECT", "Bisect", "Faster, more forgiving to bad geometry"),
|
||||
("OPENCASCADE", "OpenCASCADE", "More technically correct"),
|
||||
],
|
||||
default="BISECT",
|
||||
name="Cut Mode",
|
||||
)
|
||||
has_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
||||
|
||||
@@ -64,9 +64,15 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "calculate_shapely_surfaces")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "calculate_svgfill_surfaces")
|
||||
row.prop(props, "linework_mode")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "cut_mode")
|
||||
elif not hasattr(context.scene, "svg_export"):
|
||||
row = self.layout.row()
|
||||
row.label(text="Freestyle SVG Exporter Not Installed", icon="ERROR")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -375,9 +375,8 @@ class PolylineDecorator:
|
||||
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
||||
last_point_data = polyline_data[len(polyline_data) - 1]
|
||||
except:
|
||||
last_point_data = None
|
||||
second_to_last_point_data = None
|
||||
default_container_elevation = 0
|
||||
last_point_data = None
|
||||
|
||||
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
||||
|
||||
@@ -387,9 +386,14 @@ class PolylineDecorator:
|
||||
last_point = Vector((0, 0, 0))
|
||||
|
||||
if is_input_on:
|
||||
snap_vector = Vector(
|
||||
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
|
||||
)
|
||||
if cls.use_default_container:
|
||||
snap_vector = Vector(
|
||||
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
|
||||
)
|
||||
else:
|
||||
snap_vector = Vector(
|
||||
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"]))
|
||||
)
|
||||
else:
|
||||
if cls.use_default_container:
|
||||
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
@@ -405,18 +409,22 @@ class PolylineDecorator:
|
||||
else:
|
||||
# Creates a fake "second to last" point away from the first point but in the same x axis
|
||||
# this allows to calculate the angle relative to x axis when there is only one point
|
||||
second_to_last_point = Vector((last_point.x - 1000, last_point.y, last_point.z))
|
||||
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
|
||||
|
||||
distance = (snap_vector - last_point).length
|
||||
if distance > 0:
|
||||
angle = tool.Cad.angle_3_vectors(snap_vector, last_point, second_to_last_point, degrees=True)
|
||||
angle = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True)
|
||||
|
||||
# Round angle to the nearest 0.05
|
||||
angle = round(angle / 0.05) * 0.05
|
||||
|
||||
if cls.input_panel:
|
||||
cls.input_panel["X"] = str(round(snap_vector.x, 4))
|
||||
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
|
||||
cls.input_panel["X"] = str(round(snap_vector.x, 3))
|
||||
cls.input_panel["Y"] = str(round(snap_vector.y, 3))
|
||||
if "Z" in list(cls.input_panel.keys()):
|
||||
cls.input_panel["Z"] = str(round(snap_vector.z, 4))
|
||||
cls.input_panel["D"] = str(round(distance, 4))
|
||||
cls.input_panel["A"] = str(round(angle, 4))
|
||||
cls.input_panel["Z"] = str(round(snap_vector.z, 3))
|
||||
cls.input_panel["D"] = str(round(distance, 3))
|
||||
cls.input_panel["A"] = str(round(angle, 3))
|
||||
|
||||
return cls.input_panel
|
||||
|
||||
@@ -470,13 +478,13 @@ class PolylineDecorator:
|
||||
try:
|
||||
polyline_data = context.scene.BIMModelProperties.polyline_point
|
||||
last_point_data = polyline_data[len(polyline_data) - 1]
|
||||
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
|
||||
except:
|
||||
return
|
||||
last_point = Vector((0, 0, 0))
|
||||
|
||||
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
||||
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
|
||||
second_to_last_point = None
|
||||
|
||||
if len(polyline_data) > 1:
|
||||
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
|
||||
second_to_last_point = Vector(
|
||||
@@ -485,35 +493,33 @@ class PolylineDecorator:
|
||||
else:
|
||||
# Creates a fake "second to last" point away from the first point but in the same x axis
|
||||
# this allows to calculate the angle relative to x axis when there is only one point
|
||||
second_to_last_point = Vector((last_point.x - 10, last_point.y, last_point.z))
|
||||
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
|
||||
|
||||
distance = float(cls.input_panel["D"])
|
||||
|
||||
if distance < 0 or distance > 0:
|
||||
angle_rad = radians(180 - float(cls.input_panel["A"]))
|
||||
ref_vec = second_to_last_point - last_point
|
||||
dir_vec = last_point - snap_vector
|
||||
angle = radians(float(cls.input_panel["A"]))
|
||||
|
||||
rot_axis = ref_vec.cross(dir_vec)
|
||||
rot_axis.normalize()
|
||||
rot_axis = Vector((abs(rot_axis.x), abs(rot_axis.y), abs(rot_axis.z)))
|
||||
rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
|
||||
|
||||
rot_mat = Matrix.Rotation(angle_rad, 3, rot_axis)
|
||||
|
||||
ref_vec.normalize()
|
||||
coords = ((ref_vec @ rot_mat) * distance) + last_point
|
||||
coords = rot_vector * distance + last_point
|
||||
|
||||
x = coords[0]
|
||||
y = coords[1]
|
||||
z = coords[2]
|
||||
if cls.input_panel:
|
||||
cls.input_panel["X"] = str(round(x, 4))
|
||||
cls.input_panel["Y"] = str(round(y, 4))
|
||||
cls.input_panel["X"] = str(round(x, 3))
|
||||
cls.input_panel["Y"] = str(round(y, 3))
|
||||
if "Z" in list(cls.input_panel.keys()):
|
||||
cls.input_panel["Z"] = str(round(z, 4))
|
||||
cls.input_panel["Z"] = str(round(z, 3))
|
||||
|
||||
return cls.input_panel
|
||||
|
||||
cls.input_panel["X"] = str(round(last_point.x, 3))
|
||||
cls.input_panel["Y"] = str(round(last_point.y, 3))
|
||||
if "Z" in list(cls.input_panel.keys()):
|
||||
cls.input_panel["Z"] = str(round(last_point.z, 3))
|
||||
|
||||
return cls.input_panel
|
||||
|
||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||
@@ -522,9 +528,8 @@ class PolylineDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_input_panel(self, context):
|
||||
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
|
||||
|
||||
@classmethod
|
||||
def format_input_panel_units(cls, context, value):
|
||||
unit_system = tool.Drawing.get_unit_system()
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = context.scene.DocProperties.imperial_precision
|
||||
@@ -532,6 +537,15 @@ class PolylineDecorator:
|
||||
else:
|
||||
precision = None
|
||||
factor = 1
|
||||
if context.scene.unit_settings.length_unit == "MILLIMETERS":
|
||||
factor = 1000
|
||||
|
||||
return format_distance(
|
||||
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
||||
)
|
||||
|
||||
def draw_input_panel(self, context):
|
||||
texts = {"D": "Distance: ", "A": "Angle: ", "X": "X coord: ", "Y": "Y coord: ", "Z": "Z coord:", "AREA": "Area: "}
|
||||
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
self.font_id = 0
|
||||
@@ -546,11 +560,7 @@ class PolylineDecorator:
|
||||
|
||||
if key != "A" and key != self.input_type:
|
||||
value = float(value)
|
||||
if context.scene.unit_settings.length_unit == "MILLIMETERS":
|
||||
value = value * 1000
|
||||
formatted_value = format_distance(
|
||||
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
||||
)
|
||||
formatted_value = self.format_input_panel_units(context, value)
|
||||
else:
|
||||
formatted_value = value
|
||||
|
||||
@@ -582,25 +592,15 @@ class PolylineDecorator:
|
||||
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
|
||||
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
|
||||
|
||||
unit_system = tool.Drawing.get_unit_system()
|
||||
if unit_system == "IMPERIAL":
|
||||
precision = context.scene.DocProperties.imperial_precision
|
||||
factor = 3.28084
|
||||
else:
|
||||
precision = None
|
||||
factor = 1
|
||||
|
||||
value = measurement_prop[i].dim
|
||||
value = float(value)
|
||||
if context.scene.unit_settings.length_unit == "MILLIMETERS":
|
||||
value = value * 1000
|
||||
formatted_value = format_distance(
|
||||
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
||||
)
|
||||
formatted_value = self.format_input_panel_units(context, value)
|
||||
|
||||
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
|
||||
blf.draw(self.font_id, "d: " + formatted_value)
|
||||
|
||||
if i == 1:
|
||||
continue
|
||||
pos_angle = measurement_prop[i - 1].position
|
||||
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
|
||||
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
|
||||
|
||||
@@ -40,7 +40,8 @@ import json
|
||||
import collections
|
||||
|
||||
|
||||
def update_door_modifier_representation(context: bpy.types.Context, obj: bpy.types.Object) -> None:
|
||||
def update_door_modifier_representation(context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
props = obj.BIMDoorProperties
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -534,10 +535,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_door"
|
||||
bl_label = "Add Door"
|
||||
bl_options = {"REGISTER"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMDoorProperties
|
||||
|
||||
@@ -558,7 +558,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
|
||||
)
|
||||
update_door_modifier_representation(context, obj)
|
||||
update_door_modifier_representation(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -612,7 +612,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
update_door_modifier_representation(context, obj)
|
||||
update_door_modifier_representation(context)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
|
||||
@@ -429,7 +429,15 @@ class DrawPolylineWall(bpy.types.Operator):
|
||||
index = self.input_options.index(self.input_type)
|
||||
size = len(self.input_options)
|
||||
self.input_type = self.input_options[((index + 1) % size)]
|
||||
self.number_input = []
|
||||
|
||||
self.number_input = self.input_panel[self.input_type]
|
||||
self.number_input = list(self.number_input)
|
||||
self.number_output = "".join(self.number_input)
|
||||
if self.input_type != "A":
|
||||
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
|
||||
|
||||
self.input_panel[self.input_type] = self.number_output
|
||||
|
||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
@@ -437,7 +445,13 @@ class DrawPolylineWall(bpy.types.Operator):
|
||||
self.recalculate_inputs(context)
|
||||
self.is_input_on = True
|
||||
self.input_type = "D"
|
||||
self.number_input = []
|
||||
|
||||
self.number_input = self.input_panel[self.input_type]
|
||||
self.number_input = list(self.number_input)
|
||||
self.number_output = "".join(self.number_input)
|
||||
self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
|
||||
self.input_panel[self.input_type] = self.number_output
|
||||
|
||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
@@ -507,6 +521,7 @@ class DrawPolylineWall(bpy.types.Operator):
|
||||
|
||||
if self.is_input_on:
|
||||
if event.value == "RELEASE" and event.type in {"ESC"}:
|
||||
self.recalculate_inputs(context)
|
||||
self.is_input_on = False
|
||||
self.input_type = "OFF"
|
||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||
|
||||
@@ -36,7 +36,8 @@ from bmesh.types import BMVert
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def update_window_modifier_representation(context, obj):
|
||||
def update_window_modifier_representation(context):
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMWindowProperties
|
||||
ifc_file = tool.Ifc.get()
|
||||
@@ -427,10 +428,9 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.add_window"
|
||||
bl_label = "Add Window"
|
||||
bl_options = {"REGISTER"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
obj = context.active_object
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
props = obj.BIMWindowProperties
|
||||
|
||||
@@ -450,7 +450,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset=pset,
|
||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
|
||||
)
|
||||
update_window_modifier_representation(context, obj)
|
||||
update_window_modifier_representation(context)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -502,7 +502,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
update_window_modifier_representation(context, obj)
|
||||
update_window_modifier_representation(context)
|
||||
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
||||
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -159,16 +159,18 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
|
||||
if not self.props.total_elements:
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"{self.props.active_container.ifc_class} > No Contained Elements", icon="FILE_3D")
|
||||
row.label(text=f"{self.props.active_container.ifc_class} > No Elements", icon="FILE_3D")
|
||||
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
||||
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||
return
|
||||
|
||||
row = self.layout.row(align=True)
|
||||
row.label(
|
||||
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Contained Elements",
|
||||
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
|
||||
icon="FILE_3D",
|
||||
)
|
||||
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
||||
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||
op = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
|
||||
op.container = ifc_definition_id
|
||||
|
||||
|
||||
@@ -414,8 +414,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
|
||||
should_add_representation=False,
|
||||
)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
bpy.ops.bim.add_window(obj=obj.name)
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.add_window()
|
||||
|
||||
elif template == "DOOR":
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
@@ -429,8 +429,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
|
||||
should_add_representation=False,
|
||||
)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
bpy.ops.bim.add_door(obj=obj.name)
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.add_door()
|
||||
|
||||
elif template == "STAIR":
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
@@ -444,8 +444,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
ifc_class=ifc_class,
|
||||
should_add_representation=False,
|
||||
)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
bpy.ops.bim.add_stair()
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.add_stair()
|
||||
|
||||
elif template == "RAILING":
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
@@ -460,8 +460,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_add_representation=True,
|
||||
context=body,
|
||||
)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
bpy.ops.bim.add_railing()
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.add_railing()
|
||||
|
||||
elif template == "ROOF":
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
@@ -476,8 +476,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_add_representation=True,
|
||||
context=body,
|
||||
)
|
||||
tool.Blender.select_and_activate_single_object(context, obj)
|
||||
bpy.ops.bim.add_roof()
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.bim.add_roof()
|
||||
|
||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
|
||||
props.type_class = props.type_class
|
||||
|
||||
@@ -41,8 +41,8 @@ def disable_editing_cost_schedule(cost: tool.Cost) -> None:
|
||||
|
||||
|
||||
def remove_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
|
||||
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
|
||||
cost.remove_stored_schedule_columns(cost_schedule)
|
||||
ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule)
|
||||
|
||||
|
||||
def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None:
|
||||
@@ -398,3 +398,8 @@ def add_currency(ifc: tool.Ifc, cost: tool.Cost) -> ifcopenshell.entity_instance
|
||||
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
|
||||
ifc.run("unit.assign_unit", units=[unit])
|
||||
return unit
|
||||
|
||||
|
||||
def generate_cost_schedule_browser(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> bpy.types.Panel:
|
||||
cost_schedule_data = cost.create_cost_schedule_json(cost_schedule)
|
||||
return cost.generate_cost_schedule_browser(cost_schedule_data)
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -9,6 +9,7 @@ This chapter covers how you can help contribute to Bonsai.
|
||||
:hidden:
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
hello_world
|
||||
running_tests
|
||||
translations
|
||||
|
||||
@@ -3,20 +3,16 @@ Installation
|
||||
|
||||
There are different methods of installation, depending on your situation.
|
||||
|
||||
1. **Unstable installation** is recommended for power users helping with testing.
|
||||
2. **Bundling for Blender** is recommended for distributing the add-on.
|
||||
3. **Live development environment** is recommended for developers who are actively coding.
|
||||
4. **Packaged installation** is recommended for those who use a package manager.
|
||||
1. :ref:`guides/development/installation:Unstable installation` is recommended
|
||||
for power users helping with testing.
|
||||
2. :ref:`guides/development/installation:Bundling for blender` is recommended for distributing the add-on.
|
||||
3. :ref:`guides/development/installation:Live development environment` is
|
||||
recommended for developers who are actively coding.
|
||||
4. :ref:`guides/development/installation:Packaged installation` is recommended
|
||||
for those who use a package manager.
|
||||
|
||||
Unstable installation
|
||||
---------------------
|
||||
|
||||
**Unstable installation** is almost the same as **Stable installation**, except
|
||||
that they are typically updated every day. Simply download a daily build from
|
||||
the `GitHub releases page
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
|
||||
then follow the usual :doc:`installation
|
||||
instructions</users/quickstart/installation>`.
|
||||
System requirements
|
||||
-------------------
|
||||
|
||||
Bonsai officially supports all major 64-bit platforms, as well as the Python
|
||||
version shipped by the Blender Foundation for the most recent three major
|
||||
@@ -41,6 +37,76 @@ Other system specifications match the `Blender Requirements
|
||||
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
|
||||
but it happens.
|
||||
|
||||
Unstable installation
|
||||
---------------------
|
||||
|
||||
**Unstable installation** is almost the same as **Stable installation**, except
|
||||
that they are typically updated every day. To install the **Unstable** version:
|
||||
|
||||
1. Open up Blender, and click on ``Edit > Preferences``.
|
||||
|
||||
.. image:: /quickstart/images/install-bonsai-1.png
|
||||
|
||||
2. Select the **Get Extensions** tab, and press **Allow Online Access**.
|
||||
|
||||
.. image:: /quickstart/images/install-bonsai-2.png
|
||||
|
||||
3. Go to the `Bonsai Unstable Repository
|
||||
<https://github.com/IfcOpenShell/bonsai_unstable_repo>`__, and drag and drop
|
||||
from the appropriate link in the ``ID`` column of the table into Blender
|
||||
depending on your operating system.
|
||||
|
||||
.. image:: images/unstable-drag-drop.png
|
||||
|
||||
4. Enable **Check for Updates on Startup** to get updates for daily Bonsai
|
||||
builds automatically.
|
||||
|
||||
.. image:: images/unstable-auto-update.png
|
||||
|
||||
.. tip::
|
||||
|
||||
Instead of drag and drop, you can manually create the repository:
|
||||
|
||||
Open :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
|
||||
--> Repositories (Top Right) --> "+" Icon --> Add Remote Remository`.
|
||||
You'll see a window similar to the one above.
|
||||
|
||||
Use as URL:
|
||||
``https://raw.githubusercontent.com/IfcOpenShell/bonsai_unstable_repo/main/index.json``
|
||||
and enable **Check for Updates on Startup** if you want them.
|
||||
|
||||
5. Search for **Bonsai** in the top left search bar, then press the **Install**
|
||||
button.
|
||||
|
||||
.. image:: /quickstart/images/install-bonsai-3.png
|
||||
|
||||
.. warning::
|
||||
|
||||
Make sure the extension you install has ``raw.githubusercontent.com`` as
|
||||
it's "Repository" (not ``extensions.blender.org``).
|
||||
|
||||
.. image:: images/unstable-repo.png
|
||||
|
||||
6. Whenever a new update is available, you'll see it in the bottom right
|
||||
:menuselection:`Status Bar`
|
||||
|
||||
.. image:: images/unstable-icon.png
|
||||
|
||||
7. To update, click on the update button in :menuselection:`Topbar --> Edit -->
|
||||
Preferences --> Get Extensions`.
|
||||
|
||||
.. image:: /guides/images/update.png
|
||||
|
||||
8. After an update, be sure to restart.
|
||||
|
||||
.. image:: images/unstable-restart.png
|
||||
|
||||
If you wish to install an **Unstable** version offline, you can download a
|
||||
daily build from the `GitHub releases page
|
||||
<https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true>`__,
|
||||
then go to :menuselection:`Topbar --> Edit --> Preferences --> Get Extensions
|
||||
--> "V" Icon (top right) --> Install from Disk`.
|
||||
|
||||
Bundling for Blender
|
||||
--------------------
|
||||
|
||||
@@ -72,12 +138,13 @@ Live development environment
|
||||
----------------------------
|
||||
|
||||
One option for developers who want to actively develop from source is to follow
|
||||
the instructions from :ref:`devs/installation:Bundling for Blender`. However,
|
||||
the instructions from :ref:`guides/development/installation:Bundling for Blender`. However,
|
||||
creating a build, uninstalling the old add-on, and installing a new build is a
|
||||
slow process. Although it works, it is very slow, so we do not recommend it.
|
||||
|
||||
A more rapid approach is to follow the :ref:`devs/installation:Unstable
|
||||
installation` method, as this provides all dependencies for you out of the box.
|
||||
A more rapid approach is to follow the
|
||||
:ref:`guides/development/installation:Unstable installation` method, as this
|
||||
provides all dependencies for you out of the box.
|
||||
|
||||
Once you've done this, you can replace certain Python files that tend to be
|
||||
updated frequently with those from the Git repository. We're going to use
|
||||
|
||||
@@ -30,13 +30,13 @@ Updating
|
||||
|
||||
We always recommend to use the latest version.
|
||||
|
||||
Open up Blender, click on ``Edit > Preferences``, and select the **Get
|
||||
Extensions** tab. If an update is available, you will see a button next to the
|
||||
Open up Blender, click on :menuselection:`Topbar --> Edit --> Preferences -->
|
||||
Get Extensions`. If an update is available, you will see a button next to the
|
||||
**Bonsai** add-on.
|
||||
|
||||
Updates are typically available every 2 months. If you need something more
|
||||
frequent, check out :ref:`devs/installation:unstable installation` which is
|
||||
updated every day.
|
||||
frequent, check out :ref:`guides/development/installation:Unstable
|
||||
installation` which is updated every day.
|
||||
|
||||
.. image:: images/update.png
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -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"},
|
||||
),
|
||||
)
|
||||
@@ -52,6 +52,10 @@ class IFC_PARSE_API HeaderEntity {
|
||||
return data_.size();
|
||||
}
|
||||
|
||||
AttributeValue getArgument(size_t index) const {
|
||||
return data_.get_attribute_value(index);
|
||||
}
|
||||
|
||||
std::string toString(bool upper = false) const {
|
||||
std::stringstream stream;
|
||||
stream << datatype_;
|
||||
|
||||
@@ -136,21 +136,21 @@ class Patcher:
|
||||
if material.is_a("IfcMaterial"):
|
||||
materials = []
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
for idx, item in enumerate(material.MaterialLayers):
|
||||
for idx, item in enumerate(material.MaterialLayers or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Name", getattr(item, "Name", None)])
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
|
||||
elif material.is_a("IfcMaterialProfileSet"):
|
||||
for idx, item in enumerate(material.MaterialProfiles):
|
||||
for idx, item in enumerate(material.MaterialProfiles or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
|
||||
if category := getattr(material, "Category", None):
|
||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
|
||||
elif material.is_a("IfcMaterialConstituentSet"):
|
||||
for idx, item in enumerate(material.MaterialConstituents):
|
||||
for idx, item in enumerate(material.MaterialConstituents or []):
|
||||
material = item.Material
|
||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
|
||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.Name])
|
||||
|
||||
@@ -520,6 +520,26 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas
|
||||
}
|
||||
}
|
||||
|
||||
// Expose FileDescription and FileName header entities
|
||||
// to make them readable even if they were not filled properly before.
|
||||
// Though it is invalid IFC, technically.
|
||||
// FileSchema is not exposed as IFC file won't load if it's invalid.
|
||||
|
||||
%extend IfcParse::FileDescription {
|
||||
AttributeValue description() const { return $self->getArgument(0); }
|
||||
AttributeValue implementation_level() const { return $self->getArgument(1); }
|
||||
};
|
||||
|
||||
%extend IfcParse::FileName {
|
||||
AttributeValue name() const { return $self->getArgument(0); }
|
||||
AttributeValue time_stamp() const { return $self->getArgument(1); }
|
||||
AttributeValue author() const { return $self->getArgument(2); }
|
||||
AttributeValue organization() const { return $self->getArgument(3); }
|
||||
AttributeValue preprocessor_version() const { return $self->getArgument(4); }
|
||||
AttributeValue originating_system() const { return $self->getArgument(5); }
|
||||
AttributeValue authorization() const { return $self->getArgument(6); }
|
||||
};
|
||||
|
||||
%extend IfcParse::IfcSpfHeader {
|
||||
%pythoncode %{
|
||||
# Hide the getters with read-only property implementations
|
||||
@@ -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);
|
||||
|
||||