web-ui: enable assigning parametric quantities from queries #5369

This commit is contained in:
myoualid
2024-09-23 02:21:15 +01:00
parent 9f542c30b3
commit 193daff617
6 changed files with 357 additions and 58 deletions
@@ -194,6 +194,11 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["classification"] = data
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):
with open("templates/index.html", "r") as f:
@@ -7,8 +7,8 @@
--fontSize: 14px;
--background-color: #363636;
--button-background: #28a745;
--button-hover-background: #218838;
--highlight-over: #ED5700;
--button-hover-background: #ed5700;
--highlight-over: #ed5700;
--scrollbar-track-color: #f1f1f1;
--scrollbar-thumb-color: #888;
--scrollbar-thumb-hover-color: #555;
@@ -77,13 +77,16 @@ body {
text-align: left;
}
.floating-form table tr {
.floating-form tr {
margin-bottom: 10px;
}
.floating-form td {
padding: 3px;
}
.action-button {
position:relative;
position: relative;
background-color: var(--button-background);
border: none;
color: #fff;
@@ -120,8 +123,7 @@ body {
}
.active-btn {
background-color: #887821;
box-shadow: 0 0 10px #887821;
background-color: var(--highlight-over);
}
.action-button:hover {
@@ -166,17 +168,6 @@ body {
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 {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -192,12 +183,12 @@ body {
flex: 1;
overflow: auto;
border: 2px solid #254e3c;
box-shadow: 0 -5px 5px -0 #254e3c;
}
.floating-form .table-container {
max-height: 80%;
overflow: auto;
box-shadow: 0 5px 5px 0 #254e3c;
}
#cost-items {
@@ -213,8 +204,10 @@ body {
height: 50vh;
}
tr, td, th {
height: 100%;
tr,
td,
th {
height: 100%;
}
table {
@@ -224,7 +217,6 @@ table {
font-size: var(--fontSize);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
border: 1px solid #ddd;
}
thead {
@@ -291,7 +283,7 @@ input:focus {
color: #ddd;
}
#cost-items input{
#cost-items input {
border-radius: 6px;
padding: 6px 8px;
font-size: 14px;
@@ -300,7 +292,8 @@ input:focus {
min-width: 50px;
}
form input, form select {
form input,
form select {
border-radius: 6px;
padding: 3px 4px;
font-size: 12px;
@@ -309,7 +302,7 @@ form input, form select {
height: 100%;
max-height: 30px;
max-width: 100%;
box-sizing: border-box;
box-sizing: border-box;
}
#cost-items tr:hover,
@@ -438,7 +431,6 @@ i {
.selectedClassificationText {
color: #28a745;
}
.back-button {
@@ -450,8 +442,7 @@ i {
background-color: #ccc;
}
.classification-cell{
.classification-cell {
display: flex;
flex-direction: column;
justify-content: center;
@@ -459,7 +450,7 @@ i {
height: 100%;
}
.classification{
.classification {
height: 100%;
}
@@ -495,4 +486,78 @@ i {
#column-selector label:last-child {
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("quantities", handleEditQuantities);
socket.on("classification", handleClassificationData);
socket.on("message", handleMessage);
}
function handleEditQuantities(data) {
@@ -51,6 +52,7 @@ function handleEditQuantities(data) {
addQuantity: addQuantity,
editQuantity: editQuantity,
deleteQuantity: deleteQuantity,
assignFromQuery: assignFromQuery,
},
});
}
@@ -59,6 +61,15 @@ function addSumCostValue(costItemId) {
executeOperator({ type: "addSumCostValue", costItemId: costItemId });
}
function assignFromQuery(costItemId, query, propName) {
executeOperator({
type: "assignFromQuery",
costItemId: costItemId,
query: query,
propName: propName,
});
}
function addQuantity(costItemId, ifcClass) {
executeOperator({
type: "AddCostItemQuantity",
@@ -461,6 +472,28 @@ function deleteCostItem(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) {
const msg = {
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() {
CostUI.addSettingsMenu();
CostUI.addRibbonButton({
@@ -1000,6 +989,42 @@ export class CostUI {
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) {
const card = document.createElement("div");
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({
costItemId,
selectedProducts,
@@ -2116,9 +2170,15 @@ export class CostUI {
callbacks,
});
const QuantityQuerySection = CostUI.QuantityQuerySection({
costItemId,
callbacks,
});
form.appendChild(selectedProductsSection);
form.appendChild(assignedProductsSection);
form.appendChild(manualQuantitiesSection);
form.appendChild(QuantityQuerySection);
const summarySection = CostUI.createSummarySection({
selectedProducts,
@@ -2133,6 +2193,7 @@ export class CostUI {
selectedProductsSection,
assignedProductsSection,
manualQuantitiesSection,
QuantityQuerySection,
});
const lastActiveSection =
@@ -2207,6 +2268,7 @@ export class CostUI {
selectedProductsSection,
assignedProductsSection,
manualQuantitiesSection,
QuantityQuerySection,
}) {
const buttons = switchBar.querySelectorAll(".action-button");
@@ -2222,6 +2284,7 @@ export class CostUI {
assignedProductsSection.style.display = "none";
if (manualQuantitiesSection)
manualQuantitiesSection.style.display = "none";
if (QuantityQuerySection) QuantityQuerySection.style.display = "none";
const section = document.getElementById(sectionId);
if (section) {
@@ -2255,6 +2318,13 @@ export class CostUI {
);
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({
container: selectedProductsSection,
products: products,
@@ -2389,9 +2459,50 @@ export class CostUI {
manualQuantitiesSection.appendChild(tableContainer);
manualQuantitiesSection.appendChild(addButton);
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) {
const tr = document.createElement("tr");
tr.id = quantity.id;
@@ -2458,20 +2569,26 @@ export class CostUI {
addButton.addEventListener("click", function (e) {
e.preventDefault();
let type = quantityType;
if (!type) {
const qtoSection = document.getElementById(
"manual-quantities-section-" + costItemId
);
const dropdown = qtoSection.querySelector("select");
type = dropdown ? dropdown.value : null;
}
let type = CostUI.getQuantityTypeAttribute(quantityType);
callbacks.addQuantity(costItemId, type);
});
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({
classificationName,
classificationElements,
+65 -9
View File
@@ -16,11 +16,12 @@
# You should have received a copy of the GNU General Public License
# 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 bpy
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.tool as tool
import ifcopenshell.api.sequence
@@ -510,11 +511,11 @@ class Web(bonsai.core.tool.Web):
cost_classifications.append(cost_classification_data)
classification_data, classification_name = get_classification_data(IfcStore.classification_file)
data = {
"classification_data": classification_data,
"classification_name": classification_name,
"cost_item_id": cost_item.id(),
"cost_classifications": cost_classifications,
}
"classification_data": classification_data,
"classification_name": classification_name,
"cost_item_id": cost_item.id(),
"cost_classifications": cost_classifications,
}
cls.send_webui_data(
data=data,
data_key="classification",
@@ -523,7 +524,7 @@ class Web(bonsai.core.tool.Web):
if operator_data["type"] == "removeClassificationReference":
cost_item = ifc_file.by_id(operator_data["costItemId"])
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)
cls.load_cost_schedule_web_ui(cost_schedule)
if operator_data["type"] == "addClassificationReference":
@@ -536,8 +537,63 @@ class Web(bonsai.core.tool.Web):
classification = element
break
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)
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
def load_cost_item_quantities_ui(cls, cost_item: ifcopenshell.entity_instance) -> None:
@@ -1630,3 +1630,26 @@ def copy_deep(
else:
new[i] = attribute
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())