From 5e1fbacef704a3fc1fc094bf4fde32d0cda410b1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 26 Aug 2021 21:37:14 +1000 Subject: [PATCH] Cost values are now serialised and shown as formulas --- .../blenderbim/bim/module/cost/operator.py | 10 +- .../blenderbim/bim/module/cost/ui.py | 1 + .../ifcopenshell/api/cost/data.py | 3 + .../ifcopenshell/util/cost.py | 142 ++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/util/cost.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index a23f8caa23..243fc1f049 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -592,12 +592,15 @@ class AddCostValue(bpy.types.Operator): self.file = IfcStore.get_file() if self.cost_type == "FIXED": category = None + attributes = {"AppliedValue": 0.0} elif self.cost_type == "SUM": category = "*" + attributes = {"Category": category} elif self.cost_type == "CATEGORY": category = self.cost_category + attributes = {"Category": category} value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.file.by_id(self.parent)) - ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes={"Category": category}) + ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes=attributes) Data.load(self.file) return {"FINISHED"} @@ -646,8 +649,11 @@ class EnableEditingCostItemValue(bpy.types.Operator): def import_attributes(self, name, prop, data, context): if name == "AppliedValue": - # TODO: for now, only support simple values + # TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure) + prop = self.props.cost_value_attributes.add() prop.data_type = "float" + prop.name = "AppliedValue" + prop.is_optional = True prop.float_value = 0.0 if prop.is_null else data[name] return True if ( diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 18ea7cd3f6..c126582388 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -190,6 +190,7 @@ class BIM_PT_cost_schedules(Panel): for cost_value_id in Data.cost_items[self.props.active_cost_item_id]["CostValues"]: row = self.layout.row(align=True) + row.label(text=Data.cost_values[cost_value_id]["Formula"]) self.draw_readonly_cost_value_ui(row, cost_value_id) if self.props.active_cost_item_value_id: diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py index fdd8d811ec..dfb3cc36d9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py @@ -1,5 +1,6 @@ import ifcopenshell.util.date import ifcopenshell.util.unit +import ifcopenshell.util.cost class Data: @@ -148,6 +149,8 @@ class Data: cost_item_data["CategoryValues"].setdefault(cost_value.Category, 0) cost_item_data["CategoryValues"][cost_value.Category] += value_data["AppliedValue"] + value_data["Formula"] = ifcopenshell.util.cost.serialise_cost_value(cost_value) + cls.cost_values[cost_value.id()] = value_data for component in cost_value.Components or []: cls.load_cost_item_value(cost_item, cost_item_data, component) diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py new file mode 100644 index 0000000000..acc57e9bdb --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -0,0 +1,142 @@ +import lark + + +arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"} +symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"} + + +def serialise_cost_value(cost_value): + result = _serialise_cost_value(cost_value) + if result and result[0] == "(" and result[-1] == ")": + return result[1:-1] + return result + + +def _serialise_cost_value(cost_value): + value = "" + if cost_value.ArithmeticOperator and cost_value.Components: + operator = arithmetic_operator_symbols[cost_value.ArithmeticOperator] + serialised_components = [] + for component in cost_value.Components: + serialised_components.append(_serialise_cost_value(component)) + value = operator.join(serialised_components) + elif cost_value.AppliedValue is not None: + value = serialise_applied_value(cost_value.AppliedValue) + + category = "" + if cost_value.Category == "*": + category = "SUM" + elif cost_value.Category: + category = cost_value.Category + + if not category and not value: + value = "0" + + if category: + return f"{category}({value})" + elif cost_value.Components: + return f"({value})" + return value + + +def serialise_applied_value(applied_value): + if applied_value.is_a("IfcMonetaryMeasure"): + return str(applied_value.wrappedValue) + return "?" + + +def unserialise_cost_value(formula, cost_value): + unserialiser = CostValueUnserialiser() + result = unserialiser.parse(formula) + def map_element_to_result(element, result): + result["ifc"] = element + for i, component in enumerate(result.get("Components", [])): + if element.Components and i < len(element.Components): + map_element_to_formula(element.Components[i], result["Components"][i]) + map_element_to_result(cost_value, result) + return result + + +class CostValueUnserialiser: + def parse(self, formula): + l = lark.Lark( + """start: formula + formula: operand (operator operand)* + operand: value | category "(" formula ")" + value: NUMBER? + category: WORD? + operator: add | divide | multiply | subtract + add: "+" + divide: "/" + multiply: "*" + subtract: "-" + + // Embed common.lark for packaging + DIGIT: "0".."9" + HEXDIGIT: "a".."f"|"A".."F"|DIGIT + INT: DIGIT+ + SIGNED_INT: ["+"|"-"] INT + DECIMAL: INT "." INT? | "." INT + _EXP: ("e"|"E") SIGNED_INT + FLOAT: INT _EXP | DECIMAL _EXP? + SIGNED_FLOAT: ["+"|"-"] FLOAT + NUMBER: FLOAT | INT + SIGNED_NUMBER: ["+"|"-"] NUMBER + _STRING_INNER: /.*?/ + _STRING_ESC_INNER: _STRING_INNER /(?