mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-18 19:30:25 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into light/radiance
This commit is contained in:
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2 # https://github.com/actions/checkout
|
- uses: actions/checkout@v2 # https://github.com/actions/checkout
|
||||||
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
- uses: actions/setup-python@v2 # https://github.com/actions/setup-python
|
||||||
with:
|
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
|
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||||
- run: echo ${{ env.DATE }}
|
- run: echo ${{ env.DATE }}
|
||||||
- name: Get current date
|
- name: Get current date
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pyver: [py310, py311, py312]
|
pyver: [py311, py312]
|
||||||
config:
|
config:
|
||||||
- {
|
- {
|
||||||
name: "Windows Build",
|
name: "Windows Build",
|
||||||
@@ -32,15 +32,15 @@ jobs:
|
|||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "Linux Build",
|
name: "Linux Build",
|
||||||
short_name: linux
|
short_name: linux,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "MacOS Build",
|
name: "MacOS Build",
|
||||||
short_name: macos
|
short_name: macos,
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "MacOS ARM Build",
|
name: "MacOS ARM Build",
|
||||||
short_name: macosm1
|
short_name: macosm1,
|
||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -53,8 +53,7 @@ jobs:
|
|||||||
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: |
|
run: |
|
||||||
cd src/bonsai &&
|
cd src/bonsai && make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE
|
||||||
make dist PLATFORM=${{ matrix.config.short_name }} PYVERSION=${{ matrix.pyver }} IS_STABLE=TRUE
|
|
||||||
- name: Find zip file name
|
- name: Find zip file name
|
||||||
id: find_zip
|
id: find_zip
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ For more information, see:
|
|||||||
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
|
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
|
||||||
* [Bonsai Website](https://bonsaibim.org)
|
* [Bonsai Website](https://bonsaibim.org)
|
||||||
* [Bonsai Documentation](https://docs.bonsaibim.org/index.html)
|
* [Bonsai Documentation](https://docs.bonsaibim.org/index.html)
|
||||||
* [Add-on Installation](https://docs.bonsaibim.org/users/installation.html)
|
* [Add-on Installation](https://docs.bonsaibim.org/quickstart/installation.html)
|
||||||
* [Exploring an IFC model](https://docs.bonsaibim.org/users/exploring_an_ifc_model.html)
|
* [Exploring an IFC model](https://docs.bonsaibim.org/quickstart/explore_model.html)
|
||||||
|
|
||||||
Development is sponsored through your generous donations!
|
Development is sponsored through your generous donations!
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
# Current build commit hash.
|
# Current build commit hash.
|
||||||
OLD:=7e6607a
|
OLD:=03935a9
|
||||||
.PHONY: bump
|
.PHONY: bump
|
||||||
bump:
|
bump:
|
||||||
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
|
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
|||||||
blender_messages[sid]["cost_schedules"] = data
|
blender_messages[sid]["cost_schedules"] = data
|
||||||
await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web")
|
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):
|
async def schedules(request):
|
||||||
with open("templates/index.html", "r") as f:
|
with open("templates/index.html", "r") as f:
|
||||||
template = f.read()
|
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/card.css");
|
||||||
|
@import url("./components/contextMenu.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-family: Arial, sans-serif;
|
--font-family: Arial, sans-serif;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ function connectSocket() {
|
|||||||
socket.on("connect", handleWebConnect);
|
socket.on("connect", handleWebConnect);
|
||||||
socket.on("cost_schedules", handleCostSchedulesData);
|
socket.on("cost_schedules", handleCostSchedulesData);
|
||||||
socket.on("cost_items", handleCostItemsData);
|
socket.on("cost_items", handleCostItemsData);
|
||||||
|
socket.on("cost_values", handleCostValuesData);
|
||||||
|
socket.on("cost_value", handleCostValueData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlenderConnect(blenderId) {
|
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) {
|
function handleConnectedClients(data) {
|
||||||
$("#blender-count").text(data.length);
|
$("#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 handleThemeData(themeData) {
|
||||||
function arrayToRgbString(arr) {
|
function arrayToRgbString(arr) {
|
||||||
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
|
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
|
||||||
@@ -99,7 +136,6 @@ function setTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addCostItem(costItemId) {
|
function addCostItem(costItemId) {
|
||||||
console.log("addCostItem", costItemId);
|
|
||||||
executeOperator({ type: "addCostItem", costItemId: costItemId });
|
executeOperator({ type: "addCostItem", costItemId: costItemId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +155,7 @@ function handleCostSchedulesData(data) {
|
|||||||
const blenderId = data.blenderId;
|
const blenderId = data.blenderId;
|
||||||
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
|
const costSchedules = data.data["cost_schedules"]["cost_schedules"];
|
||||||
const currency = data.data["cost_schedules"]["currency"]["name"];
|
const currency = data.data["cost_schedules"]["currency"]["name"];
|
||||||
|
|
||||||
console.log(data.data["cost_schedules"]);
|
|
||||||
|
|
||||||
const costScheduleDiv = $("#cost-schedules");
|
const costScheduleDiv = $("#cost-schedules");
|
||||||
|
|
||||||
costSchedules.forEach((costSchedule) => {
|
costSchedules.forEach((costSchedule) => {
|
||||||
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
|
costSchedule.UpdateDate = new Date(costSchedule.UpdateDate);
|
||||||
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
|
const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate);
|
||||||
@@ -136,7 +168,6 @@ function handleCostSchedulesData(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCostItemsData(data) {
|
function handleCostItemsData(data) {
|
||||||
console.log(data);
|
|
||||||
CostUI.createCostSchedule({
|
CostUI.createCostSchedule({
|
||||||
data: data.data["cost_items"],
|
data: data.data["cost_items"],
|
||||||
blenderID: data.blenderId,
|
blenderID: data.blenderId,
|
||||||
@@ -144,6 +175,7 @@ function handleCostItemsData(data) {
|
|||||||
"addCostItem": addCostItem,
|
"addCostItem": addCostItem,
|
||||||
"selectAssignedElements": selectAssignedElements,
|
"selectAssignedElements": selectAssignedElements,
|
||||||
'editCostItemName': editCostItemName,
|
'editCostItemName': editCostItemName,
|
||||||
|
'enableEditingCostValues': enableEditingCostValues,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -166,3 +198,7 @@ function loadCostSchedule(costScheduleId, blenderId) {
|
|||||||
function getCostSchedules(blenderId) {
|
function getCostSchedules(blenderId) {
|
||||||
executeOperator({ type: "getCostSchedules" }, blenderId);
|
executeOperator({ type: "getCostSchedules" }, blenderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableEditingCostValues(costItemId) {
|
||||||
|
executeOperator({ type: "enableEditingCostValues", costItemId: costItemId});
|
||||||
|
}
|
||||||
@@ -82,7 +82,6 @@ function handleBlenderDisconnect(blenderId) {
|
|||||||
|
|
||||||
function handleConnectedClients(data) {
|
function handleConnectedClients(data) {
|
||||||
$("#blender-count").text(data.length);
|
$("#blender-count").text(data.length);
|
||||||
// console.log(data);
|
|
||||||
data.forEach(function (id) {
|
data.forEach(function (id) {
|
||||||
connectedClients[id] = {
|
connectedClients[id] = {
|
||||||
shown: false,
|
shown: false,
|
||||||
@@ -115,7 +114,6 @@ function handleThemeData(themeData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cssRule = generateCssVariableRule(themeData.theme);
|
const cssRule = generateCssVariableRule(themeData.theme);
|
||||||
console.log(cssRule);
|
|
||||||
|
|
||||||
var styleElement = $("#gantt-stylesheet")[0];
|
var styleElement = $("#gantt-stylesheet")[0];
|
||||||
if (styleElement) {
|
if (styleElement) {
|
||||||
@@ -133,7 +131,6 @@ function handleWorkScheduleData(data) {
|
|||||||
const workSchedules = data["data"]["work_schedule_info"];
|
const workSchedules = data["data"]["work_schedule_info"];
|
||||||
|
|
||||||
workSchedules.forEach((workSchedule) => {
|
workSchedules.forEach((workSchedule) => {
|
||||||
console.log(workSchedule);
|
|
||||||
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
|
const mainContainer = CostUI.text(new Date(workSchedule.CreationDate).toLocaleDateString());
|
||||||
const callback = () => loadWorkSchedule(workSchedule.id);
|
const callback = () => loadWorkSchedule(workSchedule.id);
|
||||||
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
|
const card = CostUI.createCard(workSchedule.Name,mainContainer, callback);
|
||||||
@@ -142,11 +139,8 @@ function handleWorkScheduleData(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleGanttData(data) {
|
function handleGanttData(data) {
|
||||||
console.log("running handleGanttData");
|
|
||||||
const blenderId = data["blenderId"];
|
const blenderId = data["blenderId"];
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
const filename = data["data"]["ifc_file"];
|
const filename = data["data"]["ifc_file"];
|
||||||
const ganttTasks = data["data"]["gantt_data"]["tasks"];
|
const ganttTasks = data["data"]["gantt_data"]["tasks"];
|
||||||
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
|
const ganttWorkSched = data["data"]["gantt_data"]["work_schedule"];
|
||||||
@@ -183,7 +177,6 @@ function handleDefaultData(data) {
|
|||||||
const blenderId = data["blenderId"];
|
const blenderId = data["blenderId"];
|
||||||
const isDirty = data["data"]["is_dirty"];
|
const isDirty = data["data"]["is_dirty"];
|
||||||
showWarning(blenderId, isDirty);
|
showWarning(blenderId, isDirty);
|
||||||
console.log('default data',data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to add a new gantt with data and filename
|
// Function to add a new gantt with data and filename
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
export class CostUI {
|
export class CostUI {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
createButton() {
|
|
||||||
console.log("Button created");
|
|
||||||
}
|
|
||||||
|
|
||||||
createInput() {
|
|
||||||
console.log("Input created");
|
|
||||||
}
|
|
||||||
|
|
||||||
static isCostScheduleLoaded(id) {
|
static isCostScheduleLoaded(id) {
|
||||||
const existingTable = document.getElementById('cost-items-' + id);
|
const existingTable = document.getElementById('cost-items-' + id);
|
||||||
return existingTable !== null;
|
return existingTable !== null;
|
||||||
@@ -17,7 +9,7 @@ export class CostUI {
|
|||||||
static removeCostSchedule(id) {
|
static removeCostSchedule(id) {
|
||||||
document.getElementById("cost-items-" + id).remove();
|
document.getElementById("cost-items-" + id).remove();
|
||||||
}
|
}
|
||||||
static createTable(id) {
|
static createTable(id, callbacks) {
|
||||||
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
|
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
|
||||||
|
|
||||||
const table = document.createElement("table");
|
const table = document.createElement("table");
|
||||||
@@ -51,7 +43,7 @@ export class CostUI {
|
|||||||
CostUI.addTableStyles(id);
|
CostUI.addTableStyles(id);
|
||||||
|
|
||||||
// Create context menu
|
// Create context menu
|
||||||
CostUI.createContextMenu();
|
CostUI.createContextMenu(callbacks);
|
||||||
|
|
||||||
table.get_blender_id = function() {
|
table.get_blender_id = function() {
|
||||||
return this.getAttribute("id").split("-")[2];
|
return this.getAttribute("id").split("-")[2];
|
||||||
@@ -71,52 +63,19 @@ export class CostUI {
|
|||||||
#cost-items-${id} td:not(:nth-child(1)) {
|
#cost-items-${id} td:not(:nth-child(1)) {
|
||||||
width: 100px; /* Set a fixed width for other columns */
|
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);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createContextMenu() {
|
static createContextMenu(callbacks) {
|
||||||
// Create context menu
|
// Create context menu
|
||||||
const contextMenu = document.createElement("div");
|
const contextMenu = document.createElement("div");
|
||||||
contextMenu.id = "context-menu";
|
contextMenu.id = "context-menu";
|
||||||
contextMenu.classList.add("context-menu");
|
contextMenu.classList.add("context-menu");
|
||||||
contextMenu.innerHTML = `
|
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="delete-button">Delete</button>
|
||||||
<button id="duplicate-button">Duplicate</button>
|
<button id="duplicate-button">Duplicate</button>
|
||||||
`;
|
`;
|
||||||
@@ -126,6 +85,7 @@ export class CostUI {
|
|||||||
document.addEventListener("contextmenu", function(event) {
|
document.addEventListener("contextmenu", function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const targetRow = event.target.closest("tr");
|
const targetRow = event.target.closest("tr");
|
||||||
|
const targetCell = event.target.closest("td");
|
||||||
if (targetRow && targetRow.parentElement.id === "cost-items") {
|
if (targetRow && targetRow.parentElement.id === "cost-items") {
|
||||||
const contextMenu = document.getElementById("context-menu");
|
const contextMenu = document.getElementById("context-menu");
|
||||||
contextMenu.style.display = "block";
|
contextMenu.style.display = "block";
|
||||||
@@ -134,6 +94,7 @@ export class CostUI {
|
|||||||
|
|
||||||
// Store the target row in the context menu for later use
|
// Store the target row in the context menu for later use
|
||||||
contextMenu.targetRow = targetRow;
|
contextMenu.targetRow = targetRow;
|
||||||
|
contextMenu.targetCell = targetCell;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("context-menu").style.display = "none";
|
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 targetRow = document.getElementById("context-menu").targetRow;
|
||||||
|
const targetCell = document.getElementById("context-menu").targetCell;
|
||||||
if (targetRow) {
|
if (targetRow) {
|
||||||
// Implement your edit action here
|
const costItemId = parseInt(targetRow.getAttribute("id"));
|
||||||
console.log("Edit row:", 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() {
|
document.getElementById("delete-button").addEventListener("click", function() {
|
||||||
const targetRow = document.getElementById("context-menu").targetRow;
|
const targetRow = document.getElementById("context-menu").targetRow;
|
||||||
if (targetRow) {
|
if (targetRow) {
|
||||||
@@ -246,7 +225,7 @@ export class CostUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static createCostSchedule({ data, blenderID, title, callbacks = {} }) {
|
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.createCostItem(data, tbody, 0, null, callbacks);
|
||||||
CostUI.applyExpandedState();
|
CostUI.applyExpandedState();
|
||||||
}
|
}
|
||||||
@@ -263,6 +242,35 @@ export class CostUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static createRow(obj, nestingLevel, parentID, callbacks = {}) {
|
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");
|
const row = document.createElement("tr");
|
||||||
row.setAttribute("id", obj.id);
|
row.setAttribute("id", obj.id);
|
||||||
row.setAttribute("parent-id", parentID);
|
row.setAttribute("parent-id", parentID);
|
||||||
@@ -270,6 +278,10 @@ export class CostUI {
|
|||||||
row.classList.add("nested");
|
row.classList.add("nested");
|
||||||
row.classList.add(`level-${nestingLevel}`);
|
row.classList.add(`level-${nestingLevel}`);
|
||||||
}
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createExpandButton(obj) {
|
||||||
const expandButton = document.createElement("button");
|
const expandButton = document.createElement("button");
|
||||||
expandButton.classList.add("toggle-button");
|
expandButton.classList.add("toggle-button");
|
||||||
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
if (obj.is_nested_by && obj.is_nested_by.length > 0) {
|
||||||
@@ -277,51 +289,62 @@ export class CostUI {
|
|||||||
} else {
|
} else {
|
||||||
expandButton.style.visibility = "hidden";
|
expandButton.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
//row.appendChild(expandButton);
|
|
||||||
|
|
||||||
expandButton.addEventListener("click", function() {
|
expandButton.addEventListener("click", function() {
|
||||||
CostUI.contractExpandRow.call(this, obj.id);
|
CostUI.contractExpandRow.call(this, obj.id);
|
||||||
});
|
});
|
||||||
|
return expandButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createNameCell(obj, nestingLevel, expandButton, callbacks) {
|
||||||
const nameCell = document.createElement("td");
|
const nameCell = document.createElement("td");
|
||||||
const nameInput = document.createElement("input");
|
const nameInput = document.createElement("input");
|
||||||
nameInput.value = obj.name ? obj.name : "Unnamed";
|
nameInput.value = obj.name ? obj.name : "Unnamed";
|
||||||
|
|
||||||
nameInput.addEventListener("change", function() {
|
nameInput.addEventListener("change", function() {
|
||||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
nameInput.addEventListener("keydown", function(event) {
|
nameInput.addEventListener("keydown", function(event) {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
|
nameCell.style.paddingLeft = nestingLevel * 20 + "px";
|
||||||
nameCell.appendChild(expandButton);
|
nameCell.appendChild(expandButton);
|
||||||
nameCell.appendChild(nameInput);
|
nameCell.appendChild(nameInput);
|
||||||
row.appendChild(nameCell);
|
return nameCell;
|
||||||
|
}
|
||||||
const totalCostQuantityCell = document.createElement("td");
|
|
||||||
totalCostQuantityCell.textContent = obj.TotalCostQuantity;
|
static createTableCell(content) {
|
||||||
row.appendChild(totalCostQuantityCell);
|
const cell = document.createElement("td");
|
||||||
|
cell.textContent = content;
|
||||||
const unitSymbolCell = document.createElement("td");
|
return cell;
|
||||||
unitSymbolCell.textContent = obj.UnitSymbol;
|
}
|
||||||
row.appendChild(unitSymbolCell);
|
|
||||||
|
static createTotalCostCell(obj) {
|
||||||
const totalAppliedValueCell = document.createElement("td");
|
|
||||||
totalAppliedValueCell.textContent = obj.TotalAppliedValue;
|
|
||||||
row.appendChild(totalAppliedValueCell);
|
|
||||||
|
|
||||||
const totalCostCell = document.createElement("td");
|
const totalCostCell = document.createElement("td");
|
||||||
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
|
const totalCost = parseFloat(obj.TotalCost).toFixed(2);
|
||||||
|
|
||||||
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
|
totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost;
|
||||||
|
return totalCostCell;
|
||||||
row.appendChild(totalCostCell);
|
}
|
||||||
|
|
||||||
|
static createFlexContainerCell(obj, callbacks) {
|
||||||
const divFlex = document.createElement("div");
|
const divFlex = document.createElement("div");
|
||||||
divFlex.classList.add("flex-container");
|
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");
|
const addButton = document.createElement("button");
|
||||||
addButton.textContent = "+";
|
addButton.textContent = "+";
|
||||||
addButton.classList.add("add-button");
|
addButton.classList.add("add-button");
|
||||||
@@ -329,31 +352,17 @@ export class CostUI {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
|
callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null;
|
||||||
});
|
});
|
||||||
|
return addButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createSelectButton(obj, callbacks) {
|
||||||
const selectButton = document.createElement("button");
|
const selectButton = document.createElement("button");
|
||||||
selectButton.textContent = "Select";
|
selectButton.textContent = "Select";
|
||||||
selectButton.addEventListener("click", function(e) {
|
selectButton.addEventListener("click", function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
|
callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null;
|
||||||
});
|
});
|
||||||
|
return selectButton;
|
||||||
divFlex.appendChild(addButton);
|
|
||||||
divFlex.appendChild(selectButton);
|
|
||||||
|
|
||||||
const flexContainerCell = document.createElement("td");
|
|
||||||
flexContainerCell.appendChild(divFlex);
|
|
||||||
row.appendChild(flexContainerCell);
|
|
||||||
|
|
||||||
row.get_id = function() {
|
|
||||||
return this.getAttribute("id");
|
|
||||||
};
|
|
||||||
|
|
||||||
row.get_parent = function() {
|
|
||||||
const parentId = this.getAttribute("parent-id");
|
|
||||||
return parentId ? document.getElementById(parentId) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static hideNestedRows(parentId) {
|
static hideNestedRows(parentId) {
|
||||||
@@ -461,4 +470,267 @@ export class CostUI {
|
|||||||
|
|
||||||
return card;
|
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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BlenderBIM Web UI</title>
|
<title>BlenderBIM Web UI</title>
|
||||||
<link rel="stylesheet" href="/static/css/gantt.css" id="index-stylesheet" />
|
<link rel="stylesheet" type="text/css" href="/static/css/gantt.css" id="index-stylesheet" />
|
||||||
<link rel="stylesheet" href="/static/css/components/card.css" />
|
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
id="tabulator-stylesheet"
|
id="tabulator-stylesheet"
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ classes = (
|
|||||||
operator.SelectUnassignedProducts,
|
operator.SelectUnassignedProducts,
|
||||||
operator.UnassignCostItemQuantity,
|
operator.UnassignCostItemQuantity,
|
||||||
operator.UnassignCostItemType,
|
operator.UnassignCostItemType,
|
||||||
|
operator.GenerateCostScheduleBrowser,
|
||||||
prop.CostItem,
|
prop.CostItem,
|
||||||
prop.CostItemQuantity,
|
prop.CostItemQuantity,
|
||||||
prop.CostItemType,
|
prop.CostItemType,
|
||||||
|
|||||||
@@ -265,16 +265,10 @@ class CostSchedulesData:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cost_values(cls):
|
def cost_values(cls):
|
||||||
results = []
|
|
||||||
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
ifc_id = bpy.context.scene.BIMCostProperties.active_cost_item_id
|
||||||
if not ifc_id:
|
if not ifc_id:
|
||||||
return results
|
return []
|
||||||
cost_item = tool.Ifc.get().by_id(ifc_id)
|
return ifcopenshell.util.cost.get_cost_values(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
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def quantity_types(cls):
|
def quantity_types(cls):
|
||||||
|
|||||||
@@ -790,3 +790,14 @@ class AddCurrency(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
core.add_currency(tool.Ifc, tool.Cost)
|
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.label(text="Schedule tools")
|
||||||
row1 = col.row(align=True)
|
row1 = col.row(align=True)
|
||||||
row1.alignment = "RIGHT"
|
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 = col.row(align=True)
|
||||||
row2.alignment = "RIGHT"
|
row2.alignment = "RIGHT"
|
||||||
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
|
op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned")
|
||||||
|
|||||||
@@ -204,6 +204,8 @@ def format_distance(
|
|||||||
tx_dist = ""
|
tx_dist = ""
|
||||||
if feet is not None:
|
if feet is not None:
|
||||||
tx_dist += str(feet) + "'"
|
tx_dist += str(feet) + "'"
|
||||||
|
if not feet and not add_inches:
|
||||||
|
tx_dist += str(feet) + "'"
|
||||||
if feet and add_inches:
|
if feet and add_inches:
|
||||||
tx_dist += " - "
|
tx_dist += " - "
|
||||||
if not feet and value < 0:
|
if not feet and value < 0:
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import bpy
|
import bpy
|
||||||
import json
|
import json
|
||||||
import time
|
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.svgwriter as svgwriter
|
||||||
import bonsai.bim.module.drawing.annotation as annotation
|
import bonsai.bim.module.drawing.annotation as annotation
|
||||||
import bonsai.bim.module.drawing.sheeter as sheeter
|
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
|
import bonsai.bim.export_ifc
|
||||||
from bonsai.bim.module.drawing.decoration import CutDecorator
|
from bonsai.bim.module.drawing.decoration import CutDecorator
|
||||||
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
|
from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData
|
||||||
from typing import NamedTuple, List, Union, Optional, Literal
|
from typing import NamedTuple, List, Union, Optional, Literal
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from math import radians
|
||||||
from mathutils import Vector, Color, Matrix
|
from mathutils import Vector, Color, Matrix
|
||||||
from timeit import default_timer as timer
|
from timeit import default_timer as timer
|
||||||
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
|
from bonsai.bim.module.drawing.prop import RasterStyleProperty, RASTER_STYLE_PROPERTIES_EXCLUDE
|
||||||
@@ -257,6 +255,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
self.svg_writer.camera_projection = tuple(
|
self.svg_writer.camera_projection = tuple(
|
||||||
self.camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
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)
|
self.svg_writer.setup_drawing_resource_paths(self.camera_element)
|
||||||
|
|
||||||
@@ -269,7 +268,12 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
with profile("Generate linework"):
|
with profile("Generate linework"):
|
||||||
if tool.Drawing.is_camera_orthographic():
|
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"):
|
with profile("Generate annotation"):
|
||||||
if tool.Drawing.is_camera_orthographic():
|
if tool.Drawing.is_camera_orthographic():
|
||||||
@@ -546,6 +550,156 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
self.file.createIfcDirection(forward.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]:
|
def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
|
||||||
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"):
|
||||||
return
|
return
|
||||||
@@ -632,10 +786,15 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
return svg_path
|
return svg_path
|
||||||
|
|
||||||
self.move_projection_to_bottom(root)
|
if self.camera.data.BIMCameraProperties.cut_mode == "BISECT":
|
||||||
self.merge_linework_and_add_metadata(root)
|
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
|
# shapely variant
|
||||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||||
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
|
nm = group.attrib["{http://www.ifcopenshell.org/ns}name"]
|
||||||
@@ -720,7 +879,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
path.set("class", " ".join(list(classes)))
|
path.set("class", " ".join(list(classes)))
|
||||||
group.insert(0, path)
|
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")
|
results = etree.tostring(root).decode("utf8")
|
||||||
svg_data_1 = results
|
svg_data_1 = results
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
@@ -885,7 +1044,6 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
group = root.findall(".//{http://www.w3.org/2000/svg}g")[0]
|
||||||
|
|
||||||
self.svg_writer.calculate_scale()
|
|
||||||
x_offset = self.svg_writer.raw_width / 2
|
x_offset = self.svg_writer.raw_width / 2
|
||||||
y_offset = self.svg_writer.raw_height / 2
|
y_offset = self.svg_writer.raw_height / 2
|
||||||
|
|
||||||
@@ -990,6 +1148,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
|
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.from_mesh(obj.data)
|
bm.from_mesh(obj.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
||||||
for edge in bm.edges:
|
for edge in bm.edges:
|
||||||
if not edge.is_manifold:
|
if not edge.is_manifold:
|
||||||
bm.free()
|
bm.free()
|
||||||
@@ -1010,6 +1169,11 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
except:
|
except:
|
||||||
continue
|
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):
|
def merge_linework_and_add_metadata(self, root):
|
||||||
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
|
join_criteria = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "JoinCriteria")
|
||||||
if join_criteria:
|
if join_criteria:
|
||||||
@@ -1073,10 +1237,34 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
if results:
|
if results:
|
||||||
for path in old_paths:
|
for path in old_paths:
|
||||||
path.getparent().remove(path)
|
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 result in results:
|
||||||
for geom in result.geoms:
|
for geom in result.geoms:
|
||||||
path = etree.SubElement(el, "{http://www.w3.org/2000/svg}path")
|
|
||||||
if isinstance(geom, shapely.Polygon):
|
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 = (
|
d = (
|
||||||
"M"
|
"M"
|
||||||
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
|
+ " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]])
|
||||||
@@ -1088,9 +1276,7 @@ class CreateDrawing(bpy.types.Operator):
|
|||||||
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
|
+ " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]])
|
||||||
+ " Z"
|
+ " Z"
|
||||||
)
|
)
|
||||||
elif isinstance(geom, shapely.LineString):
|
path.attrib["d"] = d
|
||||||
d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z"
|
|
||||||
path.attrib["d"] = d
|
|
||||||
|
|
||||||
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
|
# 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"):
|
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
|
||||||
@@ -1629,7 +1815,9 @@ class ActivateDrawing(bpy.types.Operator):
|
|||||||
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
||||||
bpy.ops.bim.reload_drawing_styles()
|
bpy.ops.bim.reload_drawing_styles()
|
||||||
bpy.ops.bim.activate_drawing_style()
|
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)
|
CutDecorator.install(context)
|
||||||
tool.Drawing.show_decorations()
|
tool.Drawing.show_decorations()
|
||||||
|
|
||||||
|
|||||||
@@ -390,8 +390,31 @@ class DocProperties(PropertyGroup):
|
|||||||
|
|
||||||
|
|
||||||
class BIMCameraProperties(PropertyGroup):
|
class BIMCameraProperties(PropertyGroup):
|
||||||
calculate_shapely_surfaces: BoolProperty(name="Calculate Shapely Surfaces", default=False)
|
linework_mode: EnumProperty(
|
||||||
calculate_svgfill_surfaces: BoolProperty(name="Calculate SVGFill Surfaces", default=False)
|
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_underlay: BoolProperty(name="Underlay", default=False, update=update_has_underlay)
|
||||||
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
has_linework: BoolProperty(name="Linework", default=True, update=update_has_linework)
|
||||||
has_annotation: BoolProperty(name="Annotation", default=True, update=update_has_annotation)
|
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.prop(dprops, "should_use_annotation_cache", text="", icon="FILE_REFRESH")
|
||||||
|
|
||||||
row = self.layout.row()
|
row = self.layout.row()
|
||||||
row.prop(props, "calculate_shapely_surfaces")
|
row.prop(props, "linework_mode")
|
||||||
row = self.layout.row()
|
if props.linework_mode == "OPENCASCADE":
|
||||||
row.prop(props, "calculate_svgfill_surfaces")
|
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 = self.layout.row()
|
||||||
row.prop(props, "width")
|
row.prop(props, "width")
|
||||||
|
|||||||
@@ -52,41 +52,28 @@ def get_sites(self, context):
|
|||||||
|
|
||||||
|
|
||||||
def update_latlong(self, context):
|
def update_latlong(self, context):
|
||||||
sun_props = context.scene.sun_pos_properties
|
update_sun_path(self)
|
||||||
sun_props.latitude = self.latitude
|
|
||||||
sun_props.longitude = self.longitude
|
|
||||||
update_sun_path()
|
|
||||||
|
|
||||||
|
|
||||||
def update_hourminute(self, context):
|
def update_hourminute(self, context):
|
||||||
sun_props = context.scene.sun_pos_properties
|
update_sun_path(self)
|
||||||
sun_props.time = self.hour + (self.minute / 60)
|
|
||||||
update_sun_path()
|
|
||||||
|
|
||||||
|
|
||||||
def update_date(self, context):
|
def update_date(self, context):
|
||||||
sun_props = context.scene.sun_pos_properties
|
update_sun_path(self)
|
||||||
sun_props.month = self.month
|
|
||||||
sun_props.day = self.day
|
|
||||||
update_sun_path()
|
|
||||||
|
|
||||||
|
|
||||||
def update_true_north(self, context):
|
def update_true_north(self, context):
|
||||||
sun_props = context.scene.sun_pos_properties
|
update_sun_path(self)
|
||||||
# Preserve IFC sign convention
|
|
||||||
sun_props.north_offset = radians(self.true_north * -1)
|
|
||||||
update_sun_path()
|
|
||||||
|
|
||||||
|
|
||||||
def update_sun_path_size(self, context):
|
def update_sun_path_size(self, context):
|
||||||
sun_props = context.scene.sun_pos_properties
|
update_sun_path(self)
|
||||||
sun_props.sun_distance = self.sun_path_size
|
|
||||||
update_sun_path()
|
|
||||||
|
|
||||||
|
|
||||||
def update_display_shadows(self, context):
|
def update_display_shadows(self, context):
|
||||||
if self.display_shadows:
|
if self.display_shadows:
|
||||||
update_sun_path()
|
update_sun_path(self)
|
||||||
context.scene.render.engine = "BLENDER_WORKBENCH"
|
context.scene.render.engine = "BLENDER_WORKBENCH"
|
||||||
context.scene.display.shading.light = "FLAT"
|
context.scene.display.shading.light = "FLAT"
|
||||||
context.scene.display.shading.show_shadows = True
|
context.scene.display.shading.show_shadows = True
|
||||||
@@ -102,7 +89,7 @@ def update_display_shadows(self, context):
|
|||||||
|
|
||||||
def update_display_sun_path(self, context):
|
def update_display_sun_path(self, context):
|
||||||
if self.display_sun_path:
|
if self.display_sun_path:
|
||||||
update_sun_path()
|
update_sun_path(self)
|
||||||
SolarDecorator.install(bpy.context)
|
SolarDecorator.install(bpy.context)
|
||||||
else:
|
else:
|
||||||
SolarDecorator.uninstall()
|
SolarDecorator.uninstall()
|
||||||
@@ -113,7 +100,7 @@ def update_resolution(self, context):
|
|||||||
context.scene.render.resolution_y = self.radiance_resolution_y
|
context.scene.render.resolution_y = self.radiance_resolution_y
|
||||||
|
|
||||||
|
|
||||||
def update_sun_path():
|
def update_sun_path(self):
|
||||||
if not SolarData.is_loaded:
|
if not SolarData.is_loaded:
|
||||||
SolarData.load()
|
SolarData.load()
|
||||||
|
|
||||||
@@ -122,6 +109,16 @@ def update_sun_path():
|
|||||||
|
|
||||||
props = bpy.context.scene.BIMSolarProperties
|
props = bpy.context.scene.BIMSolarProperties
|
||||||
sun_props = bpy.context.scene.sun_pos_properties
|
sun_props = bpy.context.scene.sun_pos_properties
|
||||||
|
|
||||||
|
sun_props.sun_distance = self.sun_path_size
|
||||||
|
sun_props.latitude = self.latitude
|
||||||
|
sun_props.longitude = self.longitude
|
||||||
|
sun_props.month = self.month
|
||||||
|
sun_props.day = self.day
|
||||||
|
sun_props.time = self.hour + (self.minute / 60)
|
||||||
|
# Preserve IFC sign convention
|
||||||
|
sun_props.north_offset = radians(self.true_north * -1)
|
||||||
|
|
||||||
props.timezone = tzfpy.get_tz(props.longitude, props.latitude)
|
props.timezone = tzfpy.get_tz(props.longitude, props.latitude)
|
||||||
timezone = pytz.timezone(props.timezone)
|
timezone = pytz.timezone(props.timezone)
|
||||||
dt = datetime.datetime(sun_props.year, sun_props.month, sun_props.day, props.hour, props.minute)
|
dt = datetime.datetime(sun_props.year, sun_props.month, sun_props.day, props.hour, props.minute)
|
||||||
|
|||||||
@@ -375,9 +375,8 @@ class PolylineDecorator:
|
|||||||
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
||||||
last_point_data = polyline_data[len(polyline_data) - 1]
|
last_point_data = polyline_data[len(polyline_data) - 1]
|
||||||
except:
|
except:
|
||||||
last_point_data = None
|
|
||||||
second_to_last_point_data = None
|
|
||||||
default_container_elevation = 0
|
default_container_elevation = 0
|
||||||
|
last_point_data = None
|
||||||
|
|
||||||
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
||||||
|
|
||||||
@@ -387,9 +386,14 @@ class PolylineDecorator:
|
|||||||
last_point = Vector((0, 0, 0))
|
last_point = Vector((0, 0, 0))
|
||||||
|
|
||||||
if is_input_on:
|
if is_input_on:
|
||||||
snap_vector = Vector(
|
if cls.use_default_container:
|
||||||
(float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
|
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:
|
else:
|
||||||
if cls.use_default_container:
|
if cls.use_default_container:
|
||||||
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||||
@@ -405,18 +409,22 @@ class PolylineDecorator:
|
|||||||
else:
|
else:
|
||||||
# Creates a fake "second to last" point away from the first point but in the same x axis
|
# Creates a fake "second to last" point away from the first point but in the same x axis
|
||||||
# this allows to calculate the angle relative to x axis when there is only one point
|
# this allows to calculate the angle relative to x axis when there is only one point
|
||||||
second_to_last_point = Vector((last_point.x - 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
|
distance = (snap_vector - last_point).length
|
||||||
if distance > 0:
|
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:
|
if cls.input_panel:
|
||||||
cls.input_panel["X"] = str(round(snap_vector.x, 4))
|
cls.input_panel["X"] = str(round(snap_vector.x, 3))
|
||||||
cls.input_panel["Y"] = str(round(snap_vector.y, 4))
|
cls.input_panel["Y"] = str(round(snap_vector.y, 3))
|
||||||
if "Z" in list(cls.input_panel.keys()):
|
if "Z" in list(cls.input_panel.keys()):
|
||||||
cls.input_panel["Z"] = str(round(snap_vector.z, 4))
|
cls.input_panel["Z"] = str(round(snap_vector.z, 3))
|
||||||
cls.input_panel["D"] = str(round(distance, 4))
|
cls.input_panel["D"] = str(round(distance, 3))
|
||||||
cls.input_panel["A"] = str(round(angle, 4))
|
cls.input_panel["A"] = str(round(angle, 3))
|
||||||
|
|
||||||
return cls.input_panel
|
return cls.input_panel
|
||||||
|
|
||||||
@@ -470,13 +478,13 @@ class PolylineDecorator:
|
|||||||
try:
|
try:
|
||||||
polyline_data = context.scene.BIMModelProperties.polyline_point
|
polyline_data = context.scene.BIMModelProperties.polyline_point
|
||||||
last_point_data = polyline_data[len(polyline_data) - 1]
|
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:
|
except:
|
||||||
return
|
last_point = Vector((0, 0, 0))
|
||||||
|
|
||||||
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
|
||||||
snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
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:
|
if len(polyline_data) > 1:
|
||||||
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
|
second_to_last_point_data = polyline_data[len(polyline_data) - 2]
|
||||||
second_to_last_point = Vector(
|
second_to_last_point = Vector(
|
||||||
@@ -485,35 +493,33 @@ class PolylineDecorator:
|
|||||||
else:
|
else:
|
||||||
# Creates a fake "second to last" point away from the first point but in the same x axis
|
# Creates a fake "second to last" point away from the first point but in the same x axis
|
||||||
# this allows to calculate the angle relative to x axis when there is only one point
|
# this allows to calculate the angle relative to x axis when there is only one point
|
||||||
second_to_last_point = Vector((last_point.x - 10, last_point.y, last_point.z))
|
second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
|
||||||
|
|
||||||
distance = float(cls.input_panel["D"])
|
distance = float(cls.input_panel["D"])
|
||||||
|
|
||||||
if distance < 0 or distance > 0:
|
if distance < 0 or distance > 0:
|
||||||
angle_rad = radians(180 - float(cls.input_panel["A"]))
|
angle = radians(float(cls.input_panel["A"]))
|
||||||
ref_vec = second_to_last_point - last_point
|
|
||||||
dir_vec = last_point - snap_vector
|
|
||||||
|
|
||||||
rot_axis = ref_vec.cross(dir_vec)
|
rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
|
||||||
rot_axis.normalize()
|
|
||||||
rot_axis = Vector((abs(rot_axis.x), abs(rot_axis.y), abs(rot_axis.z)))
|
|
||||||
|
|
||||||
rot_mat = Matrix.Rotation(angle_rad, 3, rot_axis)
|
coords = rot_vector * distance + last_point
|
||||||
|
|
||||||
ref_vec.normalize()
|
|
||||||
coords = ((ref_vec @ rot_mat) * distance) + last_point
|
|
||||||
|
|
||||||
x = coords[0]
|
x = coords[0]
|
||||||
y = coords[1]
|
y = coords[1]
|
||||||
z = coords[2]
|
z = coords[2]
|
||||||
if cls.input_panel:
|
if cls.input_panel:
|
||||||
cls.input_panel["X"] = str(round(x, 4))
|
cls.input_panel["X"] = str(round(x, 3))
|
||||||
cls.input_panel["Y"] = str(round(y, 4))
|
cls.input_panel["Y"] = str(round(y, 3))
|
||||||
if "Z" in list(cls.input_panel.keys()):
|
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
|
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
|
return cls.input_panel
|
||||||
|
|
||||||
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
def draw_batch(self, shader_type, content_pos, color, indices=None):
|
||||||
@@ -522,9 +528,8 @@ class PolylineDecorator:
|
|||||||
shader.uniform_float("color", color)
|
shader.uniform_float("color", color)
|
||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
def draw_input_panel(self, context):
|
@classmethod
|
||||||
texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"}
|
def format_input_panel_units(cls, context, value):
|
||||||
|
|
||||||
unit_system = tool.Drawing.get_unit_system()
|
unit_system = tool.Drawing.get_unit_system()
|
||||||
if unit_system == "IMPERIAL":
|
if unit_system == "IMPERIAL":
|
||||||
precision = context.scene.DocProperties.imperial_precision
|
precision = context.scene.DocProperties.imperial_precision
|
||||||
@@ -532,6 +537,15 @@ class PolylineDecorator:
|
|||||||
else:
|
else:
|
||||||
precision = None
|
precision = None
|
||||||
factor = 1
|
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.addon_prefs = tool.Blender.get_addon_preferences()
|
||||||
self.font_id = 0
|
self.font_id = 0
|
||||||
@@ -546,11 +560,7 @@ class PolylineDecorator:
|
|||||||
|
|
||||||
if key != "A" and key != self.input_type:
|
if key != "A" and key != self.input_type:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
if context.scene.unit_settings.length_unit == "MILLIMETERS":
|
formatted_value = self.format_input_panel_units(context, value)
|
||||||
value = value * 1000
|
|
||||||
formatted_value = format_distance(
|
|
||||||
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
formatted_value = value
|
formatted_value = value
|
||||||
|
|
||||||
@@ -582,25 +592,15 @@ class PolylineDecorator:
|
|||||||
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
|
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
|
||||||
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
|
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
|
||||||
|
|
||||||
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 = measurement_prop[i].dim
|
||||||
value = float(value)
|
value = float(value)
|
||||||
if context.scene.unit_settings.length_unit == "MILLIMETERS":
|
formatted_value = self.format_input_panel_units(context, value)
|
||||||
value = value * 1000
|
|
||||||
formatted_value = format_distance(
|
|
||||||
value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True
|
|
||||||
)
|
|
||||||
|
|
||||||
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
|
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
|
||||||
blf.draw(self.font_id, "d: " + formatted_value)
|
blf.draw(self.font_id, "d: " + formatted_value)
|
||||||
|
|
||||||
|
if i == 1:
|
||||||
|
continue
|
||||||
pos_angle = measurement_prop[i - 1].position
|
pos_angle = measurement_prop[i - 1].position
|
||||||
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
|
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
|
||||||
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
|
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ import json
|
|||||||
import collections
|
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
|
props = obj.BIMDoorProperties
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
@@ -534,10 +535,9 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.add_door"
|
bl_idname = "bim.add_door"
|
||||||
bl_label = "Add Door"
|
bl_label = "Add Door"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
obj: bpy.props.StringProperty()
|
|
||||||
|
|
||||||
def _execute(self, context):
|
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)
|
element = tool.Ifc.get_entity(obj)
|
||||||
props = obj.BIMDoorProperties
|
props = obj.BIMDoorProperties
|
||||||
|
|
||||||
@@ -558,7 +558,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
pset=pset,
|
pset=pset,
|
||||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))},
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -612,7 +612,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
props.is_editing = False
|
props.is_editing = False
|
||||||
|
|
||||||
update_door_modifier_representation(context, obj)
|
update_door_modifier_representation(context)
|
||||||
|
|
||||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
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)
|
index = self.input_options.index(self.input_type)
|
||||||
size = len(self.input_options)
|
size = len(self.input_options)
|
||||||
self.input_type = self.input_options[((index + 1) % size)]
|
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)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
@@ -437,7 +445,13 @@ class DrawPolylineWall(bpy.types.Operator):
|
|||||||
self.recalculate_inputs(context)
|
self.recalculate_inputs(context)
|
||||||
self.is_input_on = True
|
self.is_input_on = True
|
||||||
self.input_type = "D"
|
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)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
@@ -507,6 +521,7 @@ class DrawPolylineWall(bpy.types.Operator):
|
|||||||
|
|
||||||
if self.is_input_on:
|
if self.is_input_on:
|
||||||
if event.value == "RELEASE" and event.type in {"ESC"}:
|
if event.value == "RELEASE" and event.type in {"ESC"}:
|
||||||
|
self.recalculate_inputs(context)
|
||||||
self.is_input_on = False
|
self.is_input_on = False
|
||||||
self.input_type = "OFF"
|
self.input_type = "OFF"
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ from bmesh.types import BMVert
|
|||||||
from mathutils import Vector
|
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)
|
element = tool.Ifc.get_entity(obj)
|
||||||
props = obj.BIMWindowProperties
|
props = obj.BIMWindowProperties
|
||||||
ifc_file = tool.Ifc.get()
|
ifc_file = tool.Ifc.get()
|
||||||
@@ -427,10 +428,9 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_idname = "bim.add_window"
|
bl_idname = "bim.add_window"
|
||||||
bl_label = "Add Window"
|
bl_label = "Add Window"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
obj: bpy.props.StringProperty()
|
|
||||||
|
|
||||||
def _execute(self, context):
|
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)
|
element = tool.Ifc.get_entity(obj)
|
||||||
props = obj.BIMWindowProperties
|
props = obj.BIMWindowProperties
|
||||||
|
|
||||||
@@ -450,7 +450,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
pset=pset,
|
pset=pset,
|
||||||
properties={"Data": tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))},
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
@@ -502,7 +502,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
props.is_editing = False
|
props.is_editing = False
|
||||||
|
|
||||||
update_window_modifier_representation(context, obj)
|
update_window_modifier_representation(context)
|
||||||
|
|
||||||
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
pset = tool.Pset.get_element_pset(element, "BBIM_Window")
|
||||||
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
window_data = tool.Ifc.get().createIfcText(json.dumps(window_data, default=list))
|
||||||
|
|||||||
@@ -1712,7 +1712,7 @@ class LoadLinkedProject(bpy.types.Operator):
|
|||||||
mesh.polygons.foreach_set("loop_total", loop_total)
|
mesh.polygons.foreach_set("loop_total", loop_total)
|
||||||
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
|
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.polygons.foreach_set("material_index", material_ids)
|
||||||
|
|
||||||
mesh.update()
|
mesh.update()
|
||||||
@@ -2327,7 +2327,7 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
self.number_is_negative = False
|
self.number_is_negative = False
|
||||||
self.is_input_on = False
|
self.is_input_on = False
|
||||||
self.input_options = ["D", "A", "X", "Y", "Z"]
|
self.input_options = ["D", "A", "X", "Y", "Z"]
|
||||||
self.input_type = "OFF"
|
self.input_type = None
|
||||||
self.input_value_xy = [None, None]
|
self.input_value_xy = [None, None]
|
||||||
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
|
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
|
||||||
self.snap_angle = None
|
self.snap_angle = None
|
||||||
@@ -2353,9 +2353,9 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||||
elif self.input_type in {"D", "A"}:
|
elif self.input_type in {"D", "A"}:
|
||||||
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
|
self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
|
||||||
|
# self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||||
self.input_panel[self.input_type] = self.number_output
|
else:
|
||||||
|
self.input_panel[self.input_type] = self.number_output
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
@@ -2365,7 +2365,7 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
|
if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
|
||||||
self.mousemove_count += 1
|
self.mousemove_count += 1
|
||||||
self.is_input_on = False
|
self.is_input_on = False
|
||||||
self.input_type = "OFF"
|
self.input_type = None
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Snap.clear_snapping_ref()
|
tool.Snap.clear_snapping_ref()
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
@@ -2382,7 +2382,6 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
|
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
|
||||||
PolylineDecorator.set_mouse_position(event)
|
PolylineDecorator.set_mouse_position(event)
|
||||||
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
@@ -2416,7 +2415,15 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
index = self.input_options.index(self.input_type)
|
index = self.input_options.index(self.input_type)
|
||||||
size = len(self.input_options)
|
size = len(self.input_options)
|
||||||
self.input_type = self.input_options[((index + 1) % size)]
|
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)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
@@ -2424,7 +2431,13 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
self.recalculate_inputs(context)
|
self.recalculate_inputs(context)
|
||||||
self.is_input_on = True
|
self.is_input_on = True
|
||||||
self.input_type = "D"
|
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)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
@@ -2436,11 +2449,12 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
if event.value == "PRESS" and event.type in {"D", "A"} and not event.shift:
|
if event.value == "RELEASE" and event.type in {"D", "A"}:
|
||||||
self.recalculate_inputs(context)
|
self.recalculate_inputs(context)
|
||||||
self.is_input_on = True
|
self.is_input_on = True
|
||||||
self.input_type = event.type
|
self.input_type = event.type
|
||||||
self.number_input = []
|
self.number_input = []
|
||||||
|
self.input_panel[self.input_type] = ""
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
@@ -2464,12 +2478,18 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
if 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"}:
|
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
|
||||||
is_valid = self.recalculate_inputs(context)
|
is_valid = self.recalculate_inputs(context)
|
||||||
if is_valid:
|
if is_valid:
|
||||||
tool.Snap.insert_polyline_point(self.input_panel)
|
tool.Snap.insert_polyline_point(self.input_panel)
|
||||||
self.is_input_on = False
|
self.is_input_on = False
|
||||||
self.input_type = "OFF"
|
self.input_type = None
|
||||||
self.number_input = []
|
self.number_input = []
|
||||||
self.number_output = ""
|
self.number_output = ""
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
@@ -2507,8 +2527,9 @@ class MeasureTool(bpy.types.Operator):
|
|||||||
|
|
||||||
if self.is_input_on:
|
if self.is_input_on:
|
||||||
if event.value == "RELEASE" and event.type in {"ESC"}:
|
if event.value == "RELEASE" and event.type in {"ESC"}:
|
||||||
|
self.recalculate_inputs(context)
|
||||||
self.is_input_on = False
|
self.is_input_on = False
|
||||||
self.input_type = "OFF"
|
self.input_type = None
|
||||||
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -78,14 +78,18 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
bl_label = "Assign Container"
|
bl_label = "Assign Container"
|
||||||
bl_description = "Assign current default container to the selected objects"
|
bl_description = "Assign current default container to the selected objects"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
container: bpy.props.IntProperty(options={"SKIP_SAVE"})
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
props = context.active_object.BIMObjectSpatialProperties
|
props = context.active_object.BIMObjectSpatialProperties
|
||||||
if (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
|
if self.container:
|
||||||
for element_obj in context.selected_objects:
|
container = tool.Ifc.get().by_id(self.container)
|
||||||
core.assign_container(
|
elif (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)):
|
||||||
tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_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):
|
class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
|||||||
@@ -159,16 +159,18 @@ class BIM_PT_spatial_decomposition(Panel):
|
|||||||
|
|
||||||
if not self.props.total_elements:
|
if not self.props.total_elements:
|
||||||
row = self.layout.row(align=True)
|
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.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
||||||
|
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
|
||||||
return
|
return
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.label(
|
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",
|
icon="FILE_3D",
|
||||||
)
|
)
|
||||||
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
|
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 = row.operator("bim.select_decomposed_elements", icon="RESTRICT_SELECT_OFF", text="")
|
||||||
op.container = ifc_definition_id
|
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",
|
ifc_class="IfcWindowType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcWindowStyle",
|
||||||
should_add_representation=False,
|
should_add_representation=False,
|
||||||
)
|
)
|
||||||
tool.Blender.select_and_activate_single_object(context, obj)
|
with context.temp_override(active_object=obj):
|
||||||
bpy.ops.bim.add_window(obj=obj.name)
|
bpy.ops.bim.add_window()
|
||||||
|
|
||||||
elif template == "DOOR":
|
elif template == "DOOR":
|
||||||
mesh = bpy.data.meshes.new(name)
|
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",
|
ifc_class="IfcDoorType" if tool.Ifc.get_schema() != "IFC2X3" else "IfcDoorStyle",
|
||||||
should_add_representation=False,
|
should_add_representation=False,
|
||||||
)
|
)
|
||||||
tool.Blender.select_and_activate_single_object(context, obj)
|
with context.temp_override(active_object=obj):
|
||||||
bpy.ops.bim.add_door(obj=obj.name)
|
bpy.ops.bim.add_door()
|
||||||
|
|
||||||
elif template == "STAIR":
|
elif template == "STAIR":
|
||||||
mesh = bpy.data.meshes.new(name)
|
mesh = bpy.data.meshes.new(name)
|
||||||
@@ -444,8 +444,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
ifc_class=ifc_class,
|
ifc_class=ifc_class,
|
||||||
should_add_representation=False,
|
should_add_representation=False,
|
||||||
)
|
)
|
||||||
tool.Blender.select_and_activate_single_object(context, obj)
|
with context.temp_override(active_object=obj):
|
||||||
bpy.ops.bim.add_stair()
|
bpy.ops.bim.add_stair()
|
||||||
|
|
||||||
elif template == "RAILING":
|
elif template == "RAILING":
|
||||||
mesh = bpy.data.meshes.new(name)
|
mesh = bpy.data.meshes.new(name)
|
||||||
@@ -460,8 +460,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
should_add_representation=True,
|
should_add_representation=True,
|
||||||
context=body,
|
context=body,
|
||||||
)
|
)
|
||||||
tool.Blender.select_and_activate_single_object(context, obj)
|
with context.temp_override(active_object=obj):
|
||||||
bpy.ops.bim.add_railing()
|
bpy.ops.bim.add_railing()
|
||||||
|
|
||||||
elif template == "ROOF":
|
elif template == "ROOF":
|
||||||
mesh = bpy.data.meshes.new(name)
|
mesh = bpy.data.meshes.new(name)
|
||||||
@@ -476,8 +476,8 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
should_add_representation=True,
|
should_add_representation=True,
|
||||||
context=body,
|
context=body,
|
||||||
)
|
)
|
||||||
tool.Blender.select_and_activate_single_object(context, obj)
|
with context.temp_override(active_object=obj):
|
||||||
bpy.ops.bim.add_roof()
|
bpy.ops.bim.add_roof()
|
||||||
|
|
||||||
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
|
bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class)
|
||||||
props.type_class = props.type_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:
|
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)
|
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:
|
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.edit_monetary_unit", unit=unit, attributes=attributes)
|
||||||
ifc.run("unit.assign_unit", units=[unit])
|
ifc.run("unit.assign_unit", units=[unit])
|
||||||
return 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)
|
||||||
@@ -85,34 +85,39 @@ class Cad:
|
|||||||
return math.degrees(a) if degrees else a
|
return math.degrees(a) if degrees else a
|
||||||
|
|
||||||
@classmethod
|
@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.
|
> 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
|
d1 = v1 - v2
|
||||||
d2 = v2 - v3
|
d2 = v3 - v2
|
||||||
|
|
||||||
axis = d1.cross(d2)
|
d1.normalize()
|
||||||
axis.normalize()
|
d2.normalize()
|
||||||
axis = Vector((abs(axis.x), abs(axis.y), abs(axis.z)))
|
|
||||||
|
|
||||||
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)
|
a = d1.angle(d2)
|
||||||
|
|
||||||
|
|
||||||
# Determine the sign of the angle based on the provided axis
|
# Determine the sign of the angle based on the provided axis
|
||||||
if degrees:
|
# If new_angle, determine the direction of the rotation
|
||||||
a = math.degrees(a)
|
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:
|
||||||
parameter = rotation_axis.dot(axis)
|
rot_mat = Matrix.Rotation(new_angle, 3, axis)
|
||||||
|
rot_vector = (d1 @ rot_mat) if parameter else (rot_mat @ d1)
|
||||||
sign = 1 if parameter <= 0 else -1
|
return rot_vector
|
||||||
|
|
||||||
return a * sign
|
|
||||||
else:
|
else:
|
||||||
return a
|
sign = -1 if parameter else 1
|
||||||
|
|
||||||
|
if degrees:
|
||||||
|
a = math.degrees(a)
|
||||||
|
return a * sign
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_x(cls, value: float, x: float, tolerance: float | None = None) -> bool:
|
def is_x(cls, value: float, x: float, tolerance: float | None = None) -> bool:
|
||||||
|
|||||||
@@ -811,9 +811,8 @@ class Cost(bonsai.core.tool.Cost):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
|
def create_cost_schedule_json(cls, cost_schedule: ifcopenshell.entity_instance) -> dict:
|
||||||
from bonsai.bim.module.cost.data import CostSchedulesData
|
from bonsai.bim.module.cost.data import CostSchedulesData
|
||||||
if not CostSchedulesData.is_loaded:
|
CostSchedulesData.load()
|
||||||
CostSchedulesData.load()
|
cost_items = CostSchedulesData.data["cost_items"]
|
||||||
cost_items = CostSchedulesData.data["cost_items"]
|
|
||||||
data = []
|
data = []
|
||||||
for rel in cost_schedule.Controls or []:
|
for rel in cost_schedule.Controls or []:
|
||||||
for cost_item in rel.RelatedObjects 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()
|
unit = tool.Unit.get_project_currency_unit()
|
||||||
if unit:
|
if unit:
|
||||||
return {"id": unit.id(), "name": unit.Currency}
|
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")
|
||||||
@@ -524,14 +524,14 @@ class Snap(bonsai.core.tool.Snap):
|
|||||||
def validate_input(cls, input_number, input_type):
|
def validate_input(cls, input_number, input_type):
|
||||||
|
|
||||||
grammar_imperial = """
|
grammar_imperial = """
|
||||||
start: FORMULA? dim expr?
|
start: (FORMULA dim expr) | dim
|
||||||
dim: imperial
|
dim: imperial
|
||||||
|
|
||||||
FORMULA: "="
|
FORMULA: "="
|
||||||
|
|
||||||
imperial: feet? "-"? inches?
|
imperial: feet? "-"? inches?
|
||||||
feet: NUMBER? " "? fraction? "'"
|
feet: NUMBER? "-"? fraction? "'"
|
||||||
inches: NUMBER? " "? fraction? "\\""
|
inches: NUMBER? "-"? fraction? "\\""
|
||||||
fraction: NUMBER "/" NUMBER
|
fraction: NUMBER "/" NUMBER
|
||||||
|
|
||||||
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
|
expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
|
||||||
@@ -583,7 +583,10 @@ class Snap(bonsai.core.tool.Snap):
|
|||||||
|
|
||||||
def imperial(self, args):
|
def imperial(self, args):
|
||||||
if len(args) > 1:
|
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:
|
else:
|
||||||
result = args[0]
|
result = args[0]
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from bonsai.bim.module.web.data import WebData
|
|||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.tool as tool
|
import bonsai.tool as tool
|
||||||
import ifcopenshell.api.sequence
|
import ifcopenshell.api.sequence
|
||||||
|
import ifcopenshell.api.cost
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
@@ -354,17 +355,15 @@ class Web(bonsai.core.tool.Web):
|
|||||||
if operator_data["type"] == "loadCostSchedule":
|
if operator_data["type"] == "loadCostSchedule":
|
||||||
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
|
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
|
||||||
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
|
bonsai.core.cost.enable_editing_cost_items(tool.Cost, cost_schedule=cost_schedule)
|
||||||
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
|
cls.load_cost_schedule_web_ui(cost_schedule)
|
||||||
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
|
|
||||||
if operator_data["type"] == "addCostItem":
|
|
||||||
bpy.ops.bim.add_cost_item(cost_item=operator_data["costItemId"])
|
|
||||||
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
|
|
||||||
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
|
|
||||||
cls.send_webui_data(data=json_data, data_key="cost_items", event="cost_items")
|
|
||||||
if operator_data["type"] == "selectAssignedElements":
|
if operator_data["type"] == "selectAssignedElements":
|
||||||
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
||||||
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
|
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
|
||||||
tool.Spatial.select_products(products, unhide=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":
|
if operator_data["type"] == "editCostItemName":
|
||||||
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
|
||||||
tool.Ifc.run(
|
tool.Ifc.run(
|
||||||
@@ -373,6 +372,65 @@ class Web(bonsai.core.tool.Web):
|
|||||||
attributes = {"Name": operator_data["name"]}
|
attributes = {"Name": operator_data["name"]}
|
||||||
)
|
)
|
||||||
tool.Cost.load_cost_schedule_tree()
|
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
|
@classmethod
|
||||||
def handle_gantt_operator(cls, operator_data: dict) -> None:
|
def handle_gantt_operator(cls, operator_data: dict) -> None:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -9,6 +9,7 @@ This chapter covers how you can help contribute to Bonsai.
|
|||||||
:hidden:
|
:hidden:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
installation
|
||||||
hello_world
|
hello_world
|
||||||
running_tests
|
running_tests
|
||||||
translations
|
translations
|
||||||
|
|||||||
@@ -3,20 +3,16 @@ Installation
|
|||||||
|
|
||||||
There are different methods of installation, depending on your situation.
|
There are different methods of installation, depending on your situation.
|
||||||
|
|
||||||
1. **Unstable installation** is recommended for power users helping with testing.
|
1. :ref:`guides/development/installation:Unstable installation` is recommended
|
||||||
2. **Bundling for Blender** is recommended for distributing the add-on.
|
for power users helping with testing.
|
||||||
3. **Live development environment** is recommended for developers who are actively coding.
|
2. :ref:`guides/development/installation:Bundling for blender` is recommended for distributing the add-on.
|
||||||
4. **Packaged installation** is recommended for those who use a package manager.
|
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
|
System requirements
|
||||||
---------------------
|
-------------------
|
||||||
|
|
||||||
**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>`.
|
|
||||||
|
|
||||||
Bonsai officially supports all major 64-bit platforms, as well as the Python
|
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
|
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,
|
Sometimes, a build may be delayed, or contain broken code. We try to avoid this,
|
||||||
but it happens.
|
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
|
Bundling for Blender
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -72,12 +138,13 @@ Live development environment
|
|||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
One option for developers who want to actively develop from source is to follow
|
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
|
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.
|
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
|
A more rapid approach is to follow the
|
||||||
installation` method, as this provides all dependencies for you out of the box.
|
: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
|
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
|
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.
|
We always recommend to use the latest version.
|
||||||
|
|
||||||
Open up Blender, click on ``Edit > Preferences``, and select the **Get
|
Open up Blender, click on :menuselection:`Topbar --> Edit --> Preferences -->
|
||||||
Extensions** tab. If an update is available, you will see a button next to the
|
Get Extensions`. If an update is available, you will see a button next to the
|
||||||
**Bonsai** add-on.
|
**Bonsai** add-on.
|
||||||
|
|
||||||
Updates are typically available every 2 months. If you need something more
|
Updates are typically available every 2 months. If you need something more
|
||||||
frequent, check out :ref:`devs/installation:unstable installation` which is
|
frequent, check out :ref:`guides/development/installation:Unstable
|
||||||
updated every day.
|
installation` which is updated every day.
|
||||||
|
|
||||||
.. image:: images/update.png
|
.. image:: images/update.png
|
||||||
|
|
||||||
@@ -94,7 +94,8 @@ Blender geometry that represents what the model might've looked at at some
|
|||||||
point. At worst, you might be looking at a completely wrong model.
|
point. At worst, you might be looking at a completely wrong model.
|
||||||
|
|
||||||
If you continue to open and save ``.blend`` files, you will run the risk of
|
If you continue to open and save ``.blend`` files, you will run the risk of
|
||||||
editing something that doesn't actually exist in your IFC model. This will
|
editing something that doesn't actually exist in your IFC model (e.g you can
|
||||||
|
meet an error similar to "RuntimeError: Instance #1234 not found"). This will
|
||||||
create unpredictable, and sometimes unrecoverable errors.
|
create unpredictable, and sometimes unrecoverable errors.
|
||||||
|
|
||||||
To avoid this issue, only open and save IFCs.
|
To avoid this issue, only open and save IFCs.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Scenario: Viewing default solar settings
|
|||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
And I look at the "Solar Access / Shadow" panel
|
And I look at the "Solar Access / Shadow" panel
|
||||||
Then I see "Etc/GMT"
|
Then I see "Etc/GMT"
|
||||||
And I see "Sunrise: 06:02:50"
|
|
||||||
|
|
||||||
Scenario: Changing the month
|
Scenario: Changing the month
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
@@ -33,7 +32,7 @@ Scenario: Automatic timezone detection based on lat / long
|
|||||||
When I set the "Latitude" property to "10.0"
|
When I set the "Latitude" property to "10.0"
|
||||||
And I set the "Longitude" property to "20.0"
|
And I set the "Longitude" property to "20.0"
|
||||||
Then I see "Africa/Ndjamena"
|
Then I see "Africa/Ndjamena"
|
||||||
And I see "Sunrise: 05:29:43"
|
And I see "Sunrise: 05:56:42"
|
||||||
|
|
||||||
Scenario: Display the sun path
|
Scenario: Display the sun path
|
||||||
Given an empty IFC project
|
Given an empty IFC project
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Scenario: Resize to storey
|
|||||||
And the object "IfcWall/Cube" is selected
|
And the object "IfcWall/Cube" is selected
|
||||||
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
|
And the variable "storey" is "tool.Ifc.get().by_type('IfcBuildingStorey')[0].id()"
|
||||||
And I press "bim.set_default_container(container={storey})"
|
And I press "bim.set_default_container(container={storey})"
|
||||||
And I press "bim.assign_container()"
|
And I press "bim.assign_container(container={storey})"
|
||||||
When I press "bim.resize_to_storey(total_storeys=1)"
|
When I press "bim.resize_to_storey(total_storeys=1)"
|
||||||
Then nothing happens
|
Then nothing happens
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64)
|
|||||||
PLATFORMTAG:=win_amd64
|
PLATFORMTAG:=win_amd64
|
||||||
endif
|
endif
|
||||||
|
|
||||||
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-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-7e6607a-$(PLATFORM).zip
|
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-03935a9-$(PLATFORM).zip
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
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:
|
class CostValueUnserialiser:
|
||||||
def parse(self, formula: str):
|
def parse(self, formula: str):
|
||||||
l = lark.Lark(
|
l = lark.Lark(
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ class IFC_PARSE_API HeaderEntity {
|
|||||||
return data_.size();
|
return data_.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AttributeValue getArgument(size_t index) const {
|
||||||
|
return data_.get_attribute_value(index);
|
||||||
|
}
|
||||||
|
|
||||||
std::string toString(bool upper = false) const {
|
std::string toString(bool upper = false) const {
|
||||||
std::stringstream stream;
|
std::stringstream stream;
|
||||||
stream << datatype_;
|
stream << datatype_;
|
||||||
|
|||||||
@@ -136,21 +136,21 @@ class Patcher:
|
|||||||
if material.is_a("IfcMaterial"):
|
if material.is_a("IfcMaterial"):
|
||||||
materials = []
|
materials = []
|
||||||
elif material.is_a("IfcMaterialLayerSet"):
|
elif material.is_a("IfcMaterialLayerSet"):
|
||||||
for idx, item in enumerate(material.MaterialLayers):
|
for idx, item in enumerate(material.MaterialLayers or []):
|
||||||
material = item.Material
|
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} Name", getattr(item, "Name", None)])
|
||||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
|
properties.append([i, "IFC Material", f"Layer {idx + 1} Material", material.Name])
|
||||||
if category := getattr(material, "Category", None):
|
if category := getattr(material, "Category", None):
|
||||||
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
|
properties.append([i, "IFC Material", f"Layer {idx + 1} Category", category])
|
||||||
elif material.is_a("IfcMaterialProfileSet"):
|
elif material.is_a("IfcMaterialProfileSet"):
|
||||||
for idx, item in enumerate(material.MaterialProfiles):
|
for idx, item in enumerate(material.MaterialProfiles or []):
|
||||||
material = item.Material
|
material = item.Material
|
||||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
|
properties.append([i, "IFC Material", f"Profile {idx + 1} Name", item.Name])
|
||||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
|
properties.append([i, "IFC Material", f"Profile {idx + 1} Material", material.Name])
|
||||||
if category := getattr(material, "Category", None):
|
if category := getattr(material, "Category", None):
|
||||||
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
|
properties.append([i, "IFC Material", f"Profile {idx + 1} Category", category])
|
||||||
elif material.is_a("IfcMaterialConstituentSet"):
|
elif material.is_a("IfcMaterialConstituentSet"):
|
||||||
for idx, item in enumerate(material.MaterialConstituents):
|
for idx, item in enumerate(material.MaterialConstituents or []):
|
||||||
material = item.Material
|
material = item.Material
|
||||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
|
properties.append([i, "IFC Material", f"Constituent {idx + 1} Name", item.Name])
|
||||||
properties.append([i, "IFC Material", f"Constituent {idx + 1} Material", material.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 {
|
%extend IfcParse::IfcSpfHeader {
|
||||||
%pythoncode %{
|
%pythoncode %{
|
||||||
# Hide the getters with read-only property implementations
|
# Hide the getters with read-only property implementations
|
||||||
|
|||||||
Reference in New Issue
Block a user