mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +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
|
||||
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,
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user