mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 19:49:58 +00:00
web-ui: enable assigning parametric quantities from queries #5369
This commit is contained in:
@@ -194,6 +194,11 @@ class BlenderNamespace(socketio.AsyncNamespace):
|
|||||||
blender_messages[sid]["classification"] = data
|
blender_messages[sid]["classification"] = data
|
||||||
await sio.emit("classification", {"blenderId": sid, "data": data}, namespace="/web")
|
await sio.emit("classification", {"blenderId": sid, "data": data}, namespace="/web")
|
||||||
|
|
||||||
|
async def on_message(self, sid, data):
|
||||||
|
print(f"Error from Blender client {sid}")
|
||||||
|
blender_messages[sid]["error"] = data
|
||||||
|
await sio.emit("message", {"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:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
--fontSize: 14px;
|
--fontSize: 14px;
|
||||||
--background-color: #363636;
|
--background-color: #363636;
|
||||||
--button-background: #28a745;
|
--button-background: #28a745;
|
||||||
--button-hover-background: #218838;
|
--button-hover-background: #ed5700;
|
||||||
--highlight-over: #ED5700;
|
--highlight-over: #ed5700;
|
||||||
--scrollbar-track-color: #f1f1f1;
|
--scrollbar-track-color: #f1f1f1;
|
||||||
--scrollbar-thumb-color: #888;
|
--scrollbar-thumb-color: #888;
|
||||||
--scrollbar-thumb-hover-color: #555;
|
--scrollbar-thumb-hover-color: #555;
|
||||||
@@ -77,13 +77,16 @@ body {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floating-form tr {
|
||||||
.floating-form table tr {
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floating-form td {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
position:relative;
|
position: relative;
|
||||||
background-color: var(--button-background);
|
background-color: var(--button-background);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -120,8 +123,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active-btn {
|
.active-btn {
|
||||||
background-color: #887821;
|
background-color: var(--highlight-over);
|
||||||
box-shadow: 0 0 10px #887821;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button:hover {
|
.action-button:hover {
|
||||||
@@ -166,17 +168,6 @@ body {
|
|||||||
background: var(--scrollbar-thumb-hover-color);
|
background: var(--scrollbar-thumb-hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
* {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Edge, IE */
|
|
||||||
* {
|
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
@@ -192,12 +183,12 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 2px solid #254e3c;
|
border: 2px solid #254e3c;
|
||||||
box-shadow: 0 -5px 5px -0 #254e3c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-form .table-container {
|
.floating-form .table-container {
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
box-shadow: 0 5px 5px 0 #254e3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cost-items {
|
#cost-items {
|
||||||
@@ -213,8 +204,10 @@ body {
|
|||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr, td, th {
|
tr,
|
||||||
height: 100%;
|
td,
|
||||||
|
th {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -224,7 +217,6 @@ table {
|
|||||||
font-size: var(--fontSize);
|
font-size: var(--fontSize);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@@ -291,7 +283,7 @@ input:focus {
|
|||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cost-items input{
|
#cost-items input {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -300,7 +292,8 @@ input:focus {
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input, form select {
|
form input,
|
||||||
|
form select {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 3px 4px;
|
padding: 3px 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -309,7 +302,7 @@ form input, form select {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cost-items tr:hover,
|
#cost-items tr:hover,
|
||||||
@@ -438,7 +431,6 @@ i {
|
|||||||
|
|
||||||
.selectedClassificationText {
|
.selectedClassificationText {
|
||||||
color: #28a745;
|
color: #28a745;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
@@ -450,8 +442,7 @@ i {
|
|||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.classification-cell {
|
||||||
.classification-cell{
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -459,7 +450,7 @@ i {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.classification{
|
.classification {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,4 +486,78 @@ i {
|
|||||||
|
|
||||||
#column-selector label:last-child {
|
#column-selector label:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-bar .action-button {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message span {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .close-button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .close-button:hover {
|
||||||
|
background-color: #ffcccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
border: 1px solid red;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .close-button {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .close-button:hover {
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
border: 1px solid green;
|
||||||
|
background-color: #e6ffe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .close-button {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .close-button:hover {
|
||||||
|
background-color: #ccffcc;
|
||||||
|
color: darkgreen;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function connectSocket() {
|
|||||||
socket.on("cost_value", handleCostValueData);
|
socket.on("cost_value", handleCostValueData);
|
||||||
socket.on("quantities", handleEditQuantities);
|
socket.on("quantities", handleEditQuantities);
|
||||||
socket.on("classification", handleClassificationData);
|
socket.on("classification", handleClassificationData);
|
||||||
|
socket.on("message", handleMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditQuantities(data) {
|
function handleEditQuantities(data) {
|
||||||
@@ -51,6 +52,7 @@ function handleEditQuantities(data) {
|
|||||||
addQuantity: addQuantity,
|
addQuantity: addQuantity,
|
||||||
editQuantity: editQuantity,
|
editQuantity: editQuantity,
|
||||||
deleteQuantity: deleteQuantity,
|
deleteQuantity: deleteQuantity,
|
||||||
|
assignFromQuery: assignFromQuery,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,15 @@ function addSumCostValue(costItemId) {
|
|||||||
executeOperator({ type: "addSumCostValue", costItemId: costItemId });
|
executeOperator({ type: "addSumCostValue", costItemId: costItemId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assignFromQuery(costItemId, query, propName) {
|
||||||
|
executeOperator({
|
||||||
|
type: "assignFromQuery",
|
||||||
|
costItemId: costItemId,
|
||||||
|
query: query,
|
||||||
|
propName: propName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addQuantity(costItemId, ifcClass) {
|
function addQuantity(costItemId, ifcClass) {
|
||||||
executeOperator({
|
executeOperator({
|
||||||
type: "AddCostItemQuantity",
|
type: "AddCostItemQuantity",
|
||||||
@@ -461,6 +472,28 @@ function deleteCostItem(costItemId) {
|
|||||||
executeOperator({ type: "deleteCostItem", costItemId: costItemId });
|
executeOperator({ type: "deleteCostItem", costItemId: costItemId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMessage(data) {
|
||||||
|
if (data.data["message"].hasOwnProperty("error")) {
|
||||||
|
handleError(data);
|
||||||
|
} else {
|
||||||
|
handleSuccess(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(data) {
|
||||||
|
const errorMessage = data.data["message"]["error"];
|
||||||
|
const containerId = data.data["message"]["container"];
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
CostUI.Error(container, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSuccess(data) {
|
||||||
|
const successMessage = data.data["message"]["success"];
|
||||||
|
const containerId = data.data["message"]["container"];
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
CostUI.Success(container, successMessage);
|
||||||
|
}
|
||||||
|
|
||||||
function executeOperator(operator, blenderId) {
|
function executeOperator(operator, blenderId) {
|
||||||
const msg = {
|
const msg = {
|
||||||
sourcePage: "cost",
|
sourcePage: "cost",
|
||||||
|
|||||||
@@ -194,17 +194,6 @@ export class CostUI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static createRibbonBar() {
|
|
||||||
const ribbonBar = document.createElement("div");
|
|
||||||
ribbonBar.className = "switch-bar";
|
|
||||||
ribbonBar.innerHTML = `
|
|
||||||
<button class="action-button" id="selected-products-btn">Selected Products</button>
|
|
||||||
<button class="action-button" id="assigned-products-btn">Assigned Products</button>
|
|
||||||
<button class="action-button" id="manual-quantities-btn">Manual Quantities</button>
|
|
||||||
`;
|
|
||||||
return ribbonBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
static createRibbon() {
|
static createRibbon() {
|
||||||
CostUI.addSettingsMenu();
|
CostUI.addSettingsMenu();
|
||||||
CostUI.addRibbonButton({
|
CostUI.addRibbonButton({
|
||||||
@@ -1000,6 +989,42 @@ export class CostUI {
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Error(container, message) {
|
||||||
|
const existingError = container.querySelector(".error");
|
||||||
|
existingError ? existingError.remove() : null;
|
||||||
|
|
||||||
|
const error = this.Text(
|
||||||
|
message,
|
||||||
|
"fa-solid fa-exclamation-triangle",
|
||||||
|
"large"
|
||||||
|
);
|
||||||
|
error.classList.add("error", "message");
|
||||||
|
|
||||||
|
const closeButton = this.createCloseButton(() => {
|
||||||
|
error.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
error.appendChild(closeButton);
|
||||||
|
container.appendChild(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Success(container, message) {
|
||||||
|
const existingSuccess = container.querySelector(".success");
|
||||||
|
existingSuccess ? existingSuccess.remove() : null;
|
||||||
|
|
||||||
|
const success = this.Text(message, "fa-solid fa-check-circle", "large");
|
||||||
|
success.classList.add("success", "message");
|
||||||
|
|
||||||
|
const closeButton = this.createCloseButton(() => {
|
||||||
|
success.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
success.appendChild(closeButton);
|
||||||
|
container.appendChild(success);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
static createCard(id, title, mainContainer, callback) {
|
static createCard(id, title, mainContainer, callback) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.classList.add("card");
|
card.classList.add("card");
|
||||||
@@ -2077,6 +2102,35 @@ export class CostUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createRibbonBar() {
|
||||||
|
const ribbonBar = document.createElement("div");
|
||||||
|
ribbonBar.className = "switch-bar";
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
text: "Parametric Take Off",
|
||||||
|
id: "selected-products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Review Assignments",
|
||||||
|
id: "assigned-products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Manual Take Off",
|
||||||
|
id: "manual-quantities",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Query Product qto",
|
||||||
|
id: "query-quantities",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
const actionButton = CostUI.createButton(button.text);
|
||||||
|
actionButton.id = button.id + "-btn";
|
||||||
|
ribbonBar.appendChild(actionButton);
|
||||||
|
});
|
||||||
|
return ribbonBar;
|
||||||
|
}
|
||||||
|
|
||||||
static enableEditingQuantities({
|
static enableEditingQuantities({
|
||||||
costItemId,
|
costItemId,
|
||||||
selectedProducts,
|
selectedProducts,
|
||||||
@@ -2116,9 +2170,15 @@ export class CostUI {
|
|||||||
callbacks,
|
callbacks,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const QuantityQuerySection = CostUI.QuantityQuerySection({
|
||||||
|
costItemId,
|
||||||
|
callbacks,
|
||||||
|
});
|
||||||
|
|
||||||
form.appendChild(selectedProductsSection);
|
form.appendChild(selectedProductsSection);
|
||||||
form.appendChild(assignedProductsSection);
|
form.appendChild(assignedProductsSection);
|
||||||
form.appendChild(manualQuantitiesSection);
|
form.appendChild(manualQuantitiesSection);
|
||||||
|
form.appendChild(QuantityQuerySection);
|
||||||
|
|
||||||
const summarySection = CostUI.createSummarySection({
|
const summarySection = CostUI.createSummarySection({
|
||||||
selectedProducts,
|
selectedProducts,
|
||||||
@@ -2133,6 +2193,7 @@ export class CostUI {
|
|||||||
selectedProductsSection,
|
selectedProductsSection,
|
||||||
assignedProductsSection,
|
assignedProductsSection,
|
||||||
manualQuantitiesSection,
|
manualQuantitiesSection,
|
||||||
|
QuantityQuerySection,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastActiveSection =
|
const lastActiveSection =
|
||||||
@@ -2207,6 +2268,7 @@ export class CostUI {
|
|||||||
selectedProductsSection,
|
selectedProductsSection,
|
||||||
assignedProductsSection,
|
assignedProductsSection,
|
||||||
manualQuantitiesSection,
|
manualQuantitiesSection,
|
||||||
|
QuantityQuerySection,
|
||||||
}) {
|
}) {
|
||||||
const buttons = switchBar.querySelectorAll(".action-button");
|
const buttons = switchBar.querySelectorAll(".action-button");
|
||||||
|
|
||||||
@@ -2222,6 +2284,7 @@ export class CostUI {
|
|||||||
assignedProductsSection.style.display = "none";
|
assignedProductsSection.style.display = "none";
|
||||||
if (manualQuantitiesSection)
|
if (manualQuantitiesSection)
|
||||||
manualQuantitiesSection.style.display = "none";
|
manualQuantitiesSection.style.display = "none";
|
||||||
|
if (QuantityQuerySection) QuantityQuerySection.style.display = "none";
|
||||||
|
|
||||||
const section = document.getElementById(sectionId);
|
const section = document.getElementById(sectionId);
|
||||||
if (section) {
|
if (section) {
|
||||||
@@ -2255,6 +2318,13 @@ export class CostUI {
|
|||||||
);
|
);
|
||||||
selectedProductsSection.appendChild(numberOfProducts);
|
selectedProductsSection.appendChild(numberOfProducts);
|
||||||
|
|
||||||
|
const refreshButton = CostUI.createButton("Refresh", "fa-solid fa-sync");
|
||||||
|
refreshButton.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
callbacks.enableEditingQuantities(costItemId);
|
||||||
|
});
|
||||||
|
selectedProductsSection.appendChild(refreshButton);
|
||||||
|
|
||||||
const selectedProductsTable = CostUI.createProductTable({
|
const selectedProductsTable = CostUI.createProductTable({
|
||||||
container: selectedProductsSection,
|
container: selectedProductsSection,
|
||||||
products: products,
|
products: products,
|
||||||
@@ -2389,9 +2459,50 @@ export class CostUI {
|
|||||||
|
|
||||||
manualQuantitiesSection.appendChild(tableContainer);
|
manualQuantitiesSection.appendChild(tableContainer);
|
||||||
manualQuantitiesSection.appendChild(addButton);
|
manualQuantitiesSection.appendChild(addButton);
|
||||||
|
|
||||||
return manualQuantitiesSection;
|
return manualQuantitiesSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static QuantityQuerySection({ callbacks, costItemId }) {
|
||||||
|
const manualQuantityQuerySection = document.createElement("div");
|
||||||
|
manualQuantityQuerySection.classList.add("form-section");
|
||||||
|
manualQuantityQuerySection.id = "query-quantities-section-" + costItemId;
|
||||||
|
manualQuantityQuerySection.style.display = "none";
|
||||||
|
|
||||||
|
const title = CostUI.Text(
|
||||||
|
"Manual Quantities",
|
||||||
|
"fa-solid fa-ruler",
|
||||||
|
"medium"
|
||||||
|
);
|
||||||
|
manualQuantityQuerySection.appendChild(title);
|
||||||
|
|
||||||
|
const queryInput = document.createElement("input");
|
||||||
|
queryInput.type = "text";
|
||||||
|
queryInput.placeholder = "Enter a query";
|
||||||
|
queryInput.id = "query-input-" + costItemId;
|
||||||
|
manualQuantityQuerySection.appendChild(queryInput);
|
||||||
|
|
||||||
|
const propNameInput = document.createElement("input");
|
||||||
|
propNameInput.type = "text";
|
||||||
|
propNameInput.placeholder = "Enter a property name";
|
||||||
|
propNameInput.id = "prop-name-input-" + costItemId;
|
||||||
|
manualQuantityQuerySection.appendChild(propNameInput);
|
||||||
|
|
||||||
|
const queryButton = CostUI.createButton(
|
||||||
|
"Assign from Query",
|
||||||
|
"fa-solid fa-plus"
|
||||||
|
);
|
||||||
|
queryButton.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const query = queryInput.value;
|
||||||
|
const propName = propNameInput.value;
|
||||||
|
callbacks.assignFromQuery(costItemId, query, propName);
|
||||||
|
});
|
||||||
|
|
||||||
|
manualQuantityQuerySection.appendChild(queryButton);
|
||||||
|
return manualQuantityQuerySection;
|
||||||
|
}
|
||||||
|
|
||||||
static createManualQuantityRow(costItemId, quantity, callbacks) {
|
static createManualQuantityRow(costItemId, quantity, callbacks) {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.id = quantity.id;
|
tr.id = quantity.id;
|
||||||
@@ -2458,20 +2569,26 @@ export class CostUI {
|
|||||||
|
|
||||||
addButton.addEventListener("click", function (e) {
|
addButton.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let type = quantityType;
|
let type = CostUI.getQuantityTypeAttribute(quantityType);
|
||||||
if (!type) {
|
|
||||||
const qtoSection = document.getElementById(
|
|
||||||
"manual-quantities-section-" + costItemId
|
|
||||||
);
|
|
||||||
const dropdown = qtoSection.querySelector("select");
|
|
||||||
type = dropdown ? dropdown.value : null;
|
|
||||||
}
|
|
||||||
callbacks.addQuantity(costItemId, type);
|
callbacks.addQuantity(costItemId, type);
|
||||||
});
|
});
|
||||||
|
|
||||||
return addButton;
|
return addButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getQuantityTypeAttribute(quantityType) {
|
||||||
|
let type = quantityType;
|
||||||
|
if (!type) {
|
||||||
|
const qtoSection = document.getElementById(
|
||||||
|
"manual-quantities-section-" + costItemId
|
||||||
|
);
|
||||||
|
if (!qtoSection) return null;
|
||||||
|
const dropdown = qtoSection.querySelector("select");
|
||||||
|
type = dropdown ? dropdown.value : null;
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
static createCostClassificationWindow({
|
static createCostClassificationWindow({
|
||||||
classificationName,
|
classificationName,
|
||||||
classificationElements,
|
classificationElements,
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from ifcopenshell.util.classification import get_classification_data, get_references
|
from ifcopenshell.util.classification import get_classification_data, get_references
|
||||||
|
from ifcopenshell.util.selector import filter_elements
|
||||||
import ifcopenshell.util.cost
|
import ifcopenshell.util.cost
|
||||||
import bpy
|
import bpy
|
||||||
from bonsai.bim.module.web.data import WebData
|
from bonsai.bim.module.web.data import WebData
|
||||||
from ifcopenshell.util.element import get_psets, get_type
|
from ifcopenshell.util.element import get_psets, get_type, has_property
|
||||||
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
|
||||||
@@ -510,11 +511,11 @@ class Web(bonsai.core.tool.Web):
|
|||||||
cost_classifications.append(cost_classification_data)
|
cost_classifications.append(cost_classification_data)
|
||||||
classification_data, classification_name = get_classification_data(IfcStore.classification_file)
|
classification_data, classification_name = get_classification_data(IfcStore.classification_file)
|
||||||
data = {
|
data = {
|
||||||
"classification_data": classification_data,
|
"classification_data": classification_data,
|
||||||
"classification_name": classification_name,
|
"classification_name": classification_name,
|
||||||
"cost_item_id": cost_item.id(),
|
"cost_item_id": cost_item.id(),
|
||||||
"cost_classifications": cost_classifications,
|
"cost_classifications": cost_classifications,
|
||||||
}
|
}
|
||||||
cls.send_webui_data(
|
cls.send_webui_data(
|
||||||
data=data,
|
data=data,
|
||||||
data_key="classification",
|
data_key="classification",
|
||||||
@@ -523,7 +524,7 @@ class Web(bonsai.core.tool.Web):
|
|||||||
if operator_data["type"] == "removeClassificationReference":
|
if operator_data["type"] == "removeClassificationReference":
|
||||||
cost_item = ifc_file.by_id(operator_data["costItemId"])
|
cost_item = ifc_file.by_id(operator_data["costItemId"])
|
||||||
cost_schedule = tool.Cost.get_cost_schedule(cost_item)
|
cost_schedule = tool.Cost.get_cost_schedule(cost_item)
|
||||||
reference = ifc_file.by_id(operator_data["classificationId"])
|
reference = ifc_file.by_id(operator_data["classificationId"])
|
||||||
tool.Ifc.run("classification.remove_reference", products=[cost_item], reference=reference)
|
tool.Ifc.run("classification.remove_reference", products=[cost_item], reference=reference)
|
||||||
cls.load_cost_schedule_web_ui(cost_schedule)
|
cls.load_cost_schedule_web_ui(cost_schedule)
|
||||||
if operator_data["type"] == "addClassificationReference":
|
if operator_data["type"] == "addClassificationReference":
|
||||||
@@ -536,8 +537,63 @@ class Web(bonsai.core.tool.Web):
|
|||||||
classification = element
|
classification = element
|
||||||
break
|
break
|
||||||
reference = IfcStore.classification_file.by_id(operator_data["classificationId"])
|
reference = IfcStore.classification_file.by_id(operator_data["classificationId"])
|
||||||
tool.Ifc.run("classification.add_reference", products=[cost_item], reference=reference, classification=classification)
|
tool.Ifc.run(
|
||||||
|
"classification.add_reference", products=[cost_item], reference=reference, classification=classification
|
||||||
|
)
|
||||||
cls.load_cost_schedule_web_ui(cost_schedule)
|
cls.load_cost_schedule_web_ui(cost_schedule)
|
||||||
|
if operator_data["type"] == "assignFromQuery":
|
||||||
|
prop_name = operator_data["propName"]
|
||||||
|
cost_item = ifc_file.by_id(operator_data["costItemId"])
|
||||||
|
results = []
|
||||||
|
try:
|
||||||
|
results = filter_elements(ifc_file, operator_data["query"])
|
||||||
|
except Exception as e:
|
||||||
|
cls.send_webui_data(
|
||||||
|
data={
|
||||||
|
"error": "Your query is invalid. Please check the syntax and try again.",
|
||||||
|
"container": "query-quantities-section-" + str(operator_data["costItemId"]),
|
||||||
|
},
|
||||||
|
data_key="message",
|
||||||
|
event="message",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not results:
|
||||||
|
cls.send_webui_data(
|
||||||
|
data={
|
||||||
|
"error": "No elements found with the specified query. Are you sure the query is correct?",
|
||||||
|
"container": "query-quantities-section-" + str(operator_data["costItemId"]),
|
||||||
|
},
|
||||||
|
data_key="message",
|
||||||
|
event="message",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
products_with_quantity = [r for r in results if has_property(r, prop_name)]
|
||||||
|
if products_with_quantity:
|
||||||
|
tool.Ifc.run(
|
||||||
|
"cost.assign_cost_item_quantity",
|
||||||
|
cost_item=cost_item,
|
||||||
|
products=results,
|
||||||
|
prop_name=prop_name,
|
||||||
|
)
|
||||||
|
cost_schedule = tool.Cost.get_cost_schedule(cost_item)
|
||||||
|
cls.load_cost_schedule_web_ui(cost_schedule)
|
||||||
|
cls.send_webui_data(
|
||||||
|
data={
|
||||||
|
"success": f"Successfully assigned quantity from {len(products_with_quantity)} products.",
|
||||||
|
"container": "query-quantities-section-" + str(operator_data["costItemId"]),
|
||||||
|
},
|
||||||
|
data_key="message",
|
||||||
|
event="message",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cls.send_webui_data(
|
||||||
|
data={
|
||||||
|
"error": f"Found {len(results)} products, but none with the specified property '{prop_name}'.",
|
||||||
|
"container": "query-quantities-section-" + str(operator_data["costItemId"]),
|
||||||
|
},
|
||||||
|
data_key="message",
|
||||||
|
event="message",
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_cost_item_quantities_ui(cls, cost_item: ifcopenshell.entity_instance) -> None:
|
def load_cost_item_quantities_ui(cls, cost_item: ifcopenshell.entity_instance) -> None:
|
||||||
|
|||||||
@@ -1630,3 +1630,26 @@ def copy_deep(
|
|||||||
else:
|
else:
|
||||||
new[i] = attribute
|
new[i] = attribute
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def has_property(product: ifcopenshell.entity_instance, property_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a product has a property with a given name.
|
||||||
|
|
||||||
|
:param product: The IFC product
|
||||||
|
:type product: ifcopenshell.entity_instance
|
||||||
|
:param property_name: The property name
|
||||||
|
:type property_name: str
|
||||||
|
:return: True if the product has the property, False otherwise
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
product = file.by_type("IfcWall")[0]
|
||||||
|
has_property = ifcopenshell.util.element.has_property(product, "NetArea")
|
||||||
|
"""
|
||||||
|
if not property_name:
|
||||||
|
return True
|
||||||
|
qtos = get_psets(product, qtos_only=True)
|
||||||
|
return any(property_name in quantities.keys() for quantities in qtos.values())
|
||||||
Reference in New Issue
Block a user