diff --git a/src/blenderbim/blenderbim/bim/module/diff/operator.py b/src/blenderbim/blenderbim/bim/module/diff/operator.py index 4ad46c64d4..582d6e6102 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/operator.py +++ b/src/blenderbim/blenderbim/bim/module/diff/operator.py @@ -19,8 +19,10 @@ import bpy import json import logging +import ifcdiff import ifcopenshell import blenderbim.bim.handler +import blenderbim.bim.import_ifc import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore @@ -175,7 +177,7 @@ class ExecuteIfcDiff(bpy.types.Operator): blenderbim.bim.handler.refresh_ui_data() return {"FINISHED"} - def load_changed_elements(self, ifc_diff): + def load_changed_elements(self, ifc_diff: ifcdiff.IfcDiff): if not tool.Ifc.get() or self.props.active_file == "NONE" or not self.props.should_load_changed_elements: return diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py index f95f70a3b8..eaf7f454fe 100644 --- a/src/blenderbim/blenderbim/core/cost.py +++ b/src/blenderbim/blenderbim/core/cost.py @@ -17,25 +17,26 @@ # along with BlenderBIM Add-on. If not, see . from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Union, Literal if TYPE_CHECKING: import bpy import ifcopenshell + import ifcopenshell.util.cost import blenderbim.tool as tool -def add_cost_schedule(ifc: tool.Ifc, name, predefined_type): +def add_cost_schedule(ifc: tool.Ifc, name: str, predefined_type: str) -> None: ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type) -def edit_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance): +def edit_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None: attributes = cost.get_cost_schedule_attributes() ifc.run("cost.edit_cost_schedule", cost_schedule=cost_schedule, attributes=attributes) cost.disable_editing_cost_schedule() -def disable_editing_cost_schedule(cost: tool.Cost): +def disable_editing_cost_schedule(cost: tool.Cost) -> None: cost.disable_editing_cost_schedule() @@ -44,67 +45,67 @@ def remove_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopens cost.remove_stored_schedule_columns(cost_schedule) -def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance): +def enable_editing_cost_schedule_attributes(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None: cost.load_cost_schedule_attributes(cost_schedule) cost.enable_editing_cost_schedule_attributes(cost_schedule) -def enable_editing_cost_items(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance): +def enable_editing_cost_items(cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None: cost.enable_editing_cost_items(cost_schedule) cost.load_active_schedule_columns() cost.load_cost_schedule_tree() cost.play_sound() -def add_summary_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance): +def add_summary_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None: ifc.run("cost.add_cost_item", cost_schedule=cost_schedule) cost.load_cost_schedule_tree() # cost.play_sound() -def add_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def add_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: ifc.run("cost.add_cost_item", cost_item=cost_item) cost.load_cost_schedule_tree() # cost.enable_editing_cost_schedule_attributes(cost_schedule) -def expand_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def expand_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.expand_cost_item(cost_item) cost.load_cost_schedule_tree() -def expand_cost_items(cost: tool.Cost): +def expand_cost_items(cost: tool.Cost) -> None: cost.expand_cost_items() cost.load_cost_schedule_tree() -def contract_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def contract_cost_item(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.contract_cost_item(cost_item) cost.load_cost_schedule_tree() -def contract_cost_items(cost: tool.Cost): +def contract_cost_items(cost: tool.Cost) -> None: cost.contract_cost_items() cost.load_cost_schedule_tree() -def remove_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item_id: int): +def remove_cost_item(ifc: tool.Ifc, cost: tool.Cost, cost_item_id: int) -> None: cost_item = ifc.get().by_id(cost_item_id) ifc.run("cost.remove_cost_item", cost_item=cost_item) cost.clean_up_cost_item_tree(cost_item_id) cost.load_cost_schedule_tree() -def enable_editing_cost_item_attributes(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def enable_editing_cost_item_attributes(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.enable_editing_cost_item_attributes(cost_item) cost.load_cost_item_attributes(cost_item) -def disable_editing_cost_item(cost: tool.Cost): +def disable_editing_cost_item(cost: tool.Cost) -> None: cost.disable_editing_cost_item() -def edit_cost_item(ifc: tool.Ifc, cost: tool.Cost): +def edit_cost_item(ifc: tool.Ifc, cost: tool.Cost) -> None: attributes = cost.get_cost_item_attributes() ifc.run("cost.edit_cost_item", cost_item=cost.get_active_cost_item(), attributes=attributes) cost.disable_editing_cost_item() @@ -148,85 +149,104 @@ def unassign_cost_item_type( return product_types -def load_cost_item_types(cost: tool.Cost): +def load_cost_item_types(cost: tool.Cost) -> None: cost_item = cost.get_active_cost_item() cost.load_cost_item_types(cost_item) def assign_cost_item_quantity( - ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, related_object_type, prop_name -): + ifc: tool.Ifc, + cost: tool.Cost, + cost_item: ifcopenshell.entity_instance, + related_object_type: tool.Cost.RELATED_OBJECT_TYPE, + prop_name: str, +) -> None: products = cost.get_products(related_object_type) if products: ifc.run("cost.assign_cost_item_quantity", cost_item=cost_item, products=products, prop_name=prop_name) cost.load_cost_item_quantity_assignments(cost_item, related_object_type=related_object_type) -def load_cost_item_quantities(cost: tool.Cost): +def load_cost_item_quantities(cost: tool.Cost) -> None: cost.load_cost_item_quantities() -def load_cost_item_element_quantities(cost: tool.Cost): +def load_cost_item_element_quantities(cost: tool.Cost) -> None: cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PRODUCT") -def load_cost_item_task_quantities(cost: tool.Cost): +def load_cost_item_task_quantities(cost: tool.Cost) -> None: cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="PROCESS") -def load_cost_item_resource_quantities(cost: tool.Cost): +def load_cost_item_resource_quantities(cost: tool.Cost) -> None: cost_item = cost.get_highlighted_cost_item() cost.load_cost_item_quantity_assignments(cost_item, related_object_type="RESOURCE") -def assign_cost_value(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, cost_rate): +def assign_cost_value( + ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, cost_rate: ifcopenshell.entity_instance +) -> None: ifc.run("cost.assign_cost_value", cost_item=cost_item, cost_rate=cost_rate) -def load_schedule_of_rates(cost: tool.Cost, schedule_of_rates): +def load_schedule_of_rates(cost: tool.Cost, schedule_of_rates: ifcopenshell.entity_instance) -> None: cost.load_schedule_of_rates_tree(schedule_of_rates) -def unassign_cost_item_quantity(ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, products): +def unassign_cost_item_quantity( + ifc: tool.Ifc, + cost: tool.Cost, + cost_item: ifcopenshell.entity_instance, + products: list[ifcopenshell.entity_instance], +) -> None: ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products) cost.load_cost_item_quantities() -def enable_editing_cost_item_quantities(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def enable_editing_cost_item_quantities(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.enable_editing_cost_item_quantities(cost_item) -def enable_editing_cost_item_values(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def enable_editing_cost_item_values(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.enable_editing_cost_item_values(cost_item) -def add_cost_item_quantity(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, ifc_class): +def add_cost_item_quantity(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, ifc_class: str) -> None: ifc.run("cost.add_cost_item_quantity", cost_item=cost_item, ifc_class=ifc_class) -def remove_cost_item_quantity(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, physical_quantity): +def remove_cost_item_quantity( + ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance, physical_quantity: ifcopenshell.entity_instance +) -> None: ifc.run("cost.remove_cost_item_quantity", cost_item=cost_item, physical_quantity=physical_quantity) -def enable_editing_cost_item_quantity(cost: tool.Cost, physical_quantity): +def enable_editing_cost_item_quantity(cost: tool.Cost, physical_quantity: ifcopenshell.entity_instance) -> None: cost.load_cost_item_quantity_attributes(physical_quantity) cost.enable_editing_cost_item_quantity(physical_quantity) -def disable_editing_cost_item_quantity(cost: tool.Cost): +def disable_editing_cost_item_quantity(cost: tool.Cost) -> None: cost.disable_editing_cost_item_quantity() -def edit_cost_item_quantity(ifc: tool.Ifc, cost: tool.Cost, physical_quantity): +def edit_cost_item_quantity(ifc: tool.Ifc, cost: tool.Cost, physical_quantity: ifcopenshell.entity_instance) -> None: attributes = cost.get_cost_item_quantity_attributes() ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes) cost.disable_editing_cost_item_quantity() cost.load_cost_item_quantities() -def add_cost_value(ifc: tool.Ifc, cost: tool.Cost, parent, cost_type, cost_category): +def add_cost_value( + ifc: tool.Ifc, + cost: tool.Cost, + parent: ifcopenshell.entity_instance, + cost_type: Literal["FIXED", "SUM", "CATEGORY"], + cost_category: Optional[str] = None, +) -> None: value = ifc.run("cost.add_cost_value", parent=parent) ifc.run( "cost.edit_cost_value", @@ -235,84 +255,95 @@ def add_cost_value(ifc: tool.Ifc, cost: tool.Cost, parent, cost_type, cost_categ ) -def remove_cost_value(ifc: tool.Ifc, parent, cost_value): +def remove_cost_value( + ifc: tool.Ifc, parent: ifcopenshell.entity_instance, cost_value: ifcopenshell.entity_instance +) -> None: ifc.run("cost.remove_cost_value", parent=parent, cost_value=cost_value) -def enable_editing_cost_item_value(cost: tool.Cost, cost_value): +def enable_editing_cost_item_value(cost: tool.Cost, cost_value: ifcopenshell.entity_instance) -> None: cost.load_cost_item_value_attributes(cost_value) cost.enable_editing_cost_item_value(cost_value) -def disable_editing_cost_item_value(cost: tool.Cost): +def disable_editing_cost_item_value(cost: tool.Cost) -> None: cost.disable_editing_cost_item_value() -def enable_editing_cost_item_value_formula(cost: tool.Cost, cost_value): +def enable_editing_cost_item_value_formula(cost: tool.Cost, cost_value: ifcopenshell.entity_instance) -> None: cost.load_cost_item_value_formula_attributes(cost_value) cost.enable_editing_cost_item_value_formula(cost_value) -def edit_cost_item_value_formula(ifc: tool.Ifc, cost: tool.Cost, cost_value): +def edit_cost_item_value_formula(ifc: tool.Ifc, cost: tool.Cost, cost_value: ifcopenshell.entity_instance) -> None: formula = cost.get_cost_item_value_formula() ifc.run("cost.edit_cost_value_formula", cost_value=cost_value, formula=formula) cost.disable_editing_cost_item_value() -def edit_cost_value(ifc: tool.Ifc, cost: tool.Cost, cost_value): +def edit_cost_value(ifc: tool.Ifc, cost: tool.Cost, cost_value: ifcopenshell.entity_instance) -> None: attributes = cost.get_cost_value_attributes() ifc.run("cost.edit_cost_value", cost_value=cost_value, attributes=attributes) cost.disable_editing_cost_item_value() # cost.load_cost_item_values(cost.get_highlighted_cost_item()) -def copy_cost_item_values(ifc: tool.Ifc, cost: tool.Cost, source, destination): +def copy_cost_item_values( + ifc: tool.Ifc, cost: tool.Cost, source: ifcopenshell.entity_instance, destination: ifcopenshell.entity_instance +) -> None: ifc.run("cost.copy_cost_item_values", source=source, destination=destination) -def select_cost_item_products(cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance): +def select_cost_item_products(cost: tool.Cost, spatial: tool.Spatial, cost_item: ifcopenshell.entity_instance) -> None: is_deep = cost.show_nested_cost_item_elements() products = cost.get_cost_item_products(cost_item, is_deep) spatial.select_products(products) -def select_cost_schedule_products(cost: tool.Cost, spatial: tool.Spatial, cost_schedule: ifcopenshell.entity_instance): +def select_cost_schedule_products( + cost: tool.Cost, spatial: tool.Spatial, cost_schedule: ifcopenshell.entity_instance +) -> None: products = cost.get_cost_schedule_products(cost_schedule) spatial.select_products(products) -def import_cost_schedule_csv(cost: tool.Cost, file_path, is_schedule_of_rates): +def import_cost_schedule_csv(cost: tool.Cost, file_path: str, is_schedule_of_rates: bool) -> None: cost.import_cost_schedule_csv(file_path, is_schedule_of_rates) -def add_cost_column(cost: tool.Cost, name): +def add_cost_column(cost: tool.Cost, name: str) -> None: cost.add_cost_column(name) -def remove_cost_column(cost: tool.Cost, name): +def remove_cost_column(cost: tool.Cost, name: str) -> None: cost.remove_cost_column(name) -def expand_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def expand_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.expand_cost_item_rate(cost_item) -def contract_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def contract_cost_item_rate(cost: tool.Cost, cost_item: ifcopenshell.entity_instance) -> None: cost.contract_cost_item_rate(cost_item) -def calculate_cost_item_resource_value(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance): +def calculate_cost_item_resource_value(ifc: tool.Ifc, cost_item: ifcopenshell.entity_instance) -> None: ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item) -def export_cost_schedules(cost: tool.Cost, filepath: str, format: str, cost_schedule=None): +def export_cost_schedules( + cost: tool.Cost, filepath: str, format: str, cost_schedule: Union[ifcopenshell.entity_instance, None] = None +) -> Union[str, None]: cost.play_sound() return cost.export_cost_schedules(filepath, format, cost_schedule) def clear_cost_item_assignments( - ifc: tool.Ifc, cost: tool.Cost, cost_item: ifcopenshell.entity_instance, related_object_type -): + ifc: tool.Ifc, + cost: tool.Cost, + cost_item: ifcopenshell.entity_instance, + related_object_type: ifcopenshell.util.cost.FILTER_BY_TYPE, +) -> None: products = cost.get_cost_item_assignments(cost_item, filter_by_type=related_object_type) if products: ifc.run("cost.unassign_cost_item_quantity", cost_item=cost_item, products=products) @@ -320,7 +351,7 @@ def clear_cost_item_assignments( cost.load_cost_schedule_tree() -def select_unassigned_products(ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial): +def select_unassigned_products(ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spatial) -> None: spatial.deselect_objects() products = ifc.get().by_type("IfcElement") cost_schedule = cost.get_active_cost_schedule() @@ -328,11 +359,13 @@ def select_unassigned_products(ifc: tool.Ifc, cost: tool.Cost, spatial: tool.Spa spatial.select_products(selection) -def load_product_cost_items(cost: tool.Cost, product): +def load_product_cost_items(cost: tool.Cost, product: ifcopenshell.entity_instance) -> None: cost.load_product_cost_items(product) -def highlight_product_cost_item(spatial: tool.Spatial, cost: tool.Cost, cost_item: ifcopenshell.entity_instance): +def highlight_product_cost_item( + spatial: tool.Spatial, cost: tool.Cost, cost_item: ifcopenshell.entity_instance +) -> Union[str, None]: cost_schedule = cost.get_cost_schedule(cost_item) is_cost_schedule_active = cost.is_cost_schedule_active(cost_schedule) if is_cost_schedule_active: @@ -341,7 +374,7 @@ def highlight_product_cost_item(spatial: tool.Spatial, cost: tool.Cost, cost_ite return "Cost schedule is not active" -def change_parent_cost_item(ifc: tool.Ifc, cost: tool.Cost, new_parent): +def change_parent_cost_item(ifc: tool.Ifc, cost: tool.Cost, new_parent) -> Union[str, None]: cost_item = cost.get_active_cost_item() if cost_item and cost.is_root_cost_item(cost_item): return "Cannot change root cost item" @@ -351,7 +384,7 @@ def change_parent_cost_item(ifc: tool.Ifc, cost: tool.Cost, new_parent): cost.load_cost_schedule_tree() -def copy_cost_item(ifc: tool.Ifc, cost: tool.Cost): +def copy_cost_item(ifc: tool.Ifc, cost: tool.Cost) -> None: cost_item = cost.get_highlighted_cost_item() if cost_item: cost_item = ifc.run("cost.copy_cost_item", cost_item=cost_item) @@ -359,7 +392,7 @@ def copy_cost_item(ifc: tool.Ifc, cost: tool.Cost): cost.load_cost_schedule_tree() -def add_currency(ifc: tool.Ifc, cost: tool.Cost): +def add_currency(ifc: tool.Ifc, cost: tool.Cost) -> ifcopenshell.entity_instance: unit = ifc.run("unit.add_monetary_unit") attributes = cost.get_currency_attributes() ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes) diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index c05e255982..56c231e84c 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -9,17 +9,20 @@ import ifcopenshell.util.cost import ifcopenshell.util.unit import blenderbim.bim.helper import json -from typing import Optional, Any, Generator, Union +from typing import Optional, Any, Generator, Union, Literal class Cost(blenderbim.core.tool.Cost): + + RELATED_OBJECT_TYPE = Literal["PRODUCT", "PROCESS", "RESOURCE"] + @classmethod - def get_cost_schedule_attributes(cls): + def get_cost_schedule_attributes(cls) -> dict[str, Any]: props = bpy.context.scene.BIMCostProperties return blenderbim.bim.helper.export_attributes(props.cost_schedule_attributes) @classmethod - def disable_editing_cost_schedule(cls): + def disable_editing_cost_schedule(cls) -> None: cls.store_active_schedule_columns() bpy.context.scene.BIMCostProperties.active_cost_schedule_id = 0 cls.disable_editing_cost_item() @@ -74,12 +77,12 @@ class Cost(blenderbim.core.tool.Cost): storage.remove(storage_col_i) @classmethod - def enable_editing_cost_schedule_attributes(cls, cost_schedule): + def enable_editing_cost_schedule_attributes(cls, cost_schedule: ifcopenshell.entity_instance) -> None: bpy.context.scene.BIMCostProperties.active_cost_schedule_id = cost_schedule.id() bpy.context.scene.BIMCostProperties.is_editing = "COST_SCHEDULE_ATTRIBUTES" @classmethod - def load_cost_schedule_attributes(cls, cost_schedule): + def load_cost_schedule_attributes(cls, cost_schedule: ifcopenshell.entity_instance) -> None: def special_import(name, prop, data): if name in ["SubmittedOn", "UpdateDate"]: prop.string_value = "" if prop.is_null else ifcopenshell.util.date.ifc2datetime(data[name]).isoformat() @@ -96,12 +99,12 @@ class Cost(blenderbim.core.tool.Cost): props.is_editing = "COST_ITEMS" @classmethod - def play_sound(cls): + def play_sound(cls) -> None: if tool.Blender.get_addon_preferences().should_play_chaching_sound: cls.play_chaching_sound() # lol @classmethod - def play_chaching_sound(cls): + def play_chaching_sound(cls) -> None: # TODO: make pitch higher as costs rise try: import aud @@ -118,7 +121,7 @@ class Cost(blenderbim.core.tool.Cost): pass # ah well @classmethod - def load_cost_schedule_tree(cls): + def load_cost_schedule_tree(cls) -> None: props = bpy.context.scene.BIMCostProperties props.is_cost_update_enabled = False cost_schedule = tool.Ifc.get().by_id(props.active_cost_schedule_id) @@ -132,7 +135,7 @@ class Cost(blenderbim.core.tool.Cost): props.is_cost_update_enabled = True @classmethod - def expand_cost_item(cls, cost_item): + def expand_cost_item(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) @@ -141,7 +144,7 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def expand_cost_items(cls): + def expand_cost_items(cls) -> None: props = bpy.context.scene.BIMCostProperties cls.contracted_cost_items = json.loads(props.contracted_cost_items) for cost_item in props.cost_items: @@ -150,7 +153,7 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def contract_cost_item(cls, cost_item): + def contract_cost_item(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) @@ -158,7 +161,7 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def contract_cost_items(cls): + def contract_cost_items(cls) -> None: props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) @@ -168,7 +171,7 @@ class Cost(blenderbim.core.tool.Cost): props.contracted_cost_items = json.dumps(cls.contracted_cost_items) @classmethod - def clean_up_cost_item_tree(cls, cost_item_id): + def clean_up_cost_item_tree(cls, cost_item_id: int) -> None: props = bpy.context.scene.BIMCostProperties if not hasattr(cls, "contracted_cost_items"): cls.contracted_cost_items = json.loads(props.contracted_cost_items) @@ -180,23 +183,23 @@ class Cost(blenderbim.core.tool.Cost): cls.enable_editing_cost_items(cost_schedule=tool.Ifc.get().by_id(props.active_cost_schedule_id)) @classmethod - def enable_editing_cost_item_attributes(cls, cost_item): + def enable_editing_cost_item_attributes(cls, cost_item: ifcopenshell.entity_instance): bpy.context.scene.BIMCostProperties.active_cost_item_id = cost_item.id() bpy.context.scene.BIMCostProperties.cost_item_editing_type = "ATTRIBUTES" @classmethod - def load_cost_item_attributes(cls, cost_item): + def load_cost_item_attributes(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.cost_item_attributes.clear() blenderbim.bim.helper.import_attributes2(cost_item, props.cost_item_attributes) @classmethod - def disable_editing_cost_item(cls): + def disable_editing_cost_item(cls) -> None: bpy.context.scene.BIMCostProperties.active_cost_item_id = 0 bpy.context.scene.BIMCostProperties.change_cost_item_parent = False @classmethod - def get_cost_item_attributes(cls): + def get_cost_item_attributes(cls) -> dict[str, Any]: props = bpy.context.scene.BIMCostProperties return blenderbim.bim.helper.export_attributes(props.cost_item_attributes) @@ -208,7 +211,7 @@ class Cost(blenderbim.core.tool.Cost): return tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_item_id) @classmethod - def get_highlighted_cost_item(cls): + def get_highlighted_cost_item(cls) -> Union[ifcopenshell.entity_instance, None]: props = bpy.context.scene.BIMCostProperties if not props.active_cost_schedule_id: return @@ -240,7 +243,9 @@ class Cost(blenderbim.core.tool.Cost): new.name = related_object.Name or "Unnamed" @classmethod - def load_cost_item_quantity_assignments(cls, cost_item: ifcopenshell.entity_instance, related_object_type): + def load_cost_item_quantity_assignments( + cls, cost_item: ifcopenshell.entity_instance, related_object_type: RELATED_OBJECT_TYPE + ) -> None: def create_list_items(collection, cost_item, is_deep): products = cls.get_cost_item_assignments(cost_item, filter_by_type=related_object_type, is_deep=False) for product in products: @@ -291,9 +296,9 @@ class Cost(blenderbim.core.tool.Cost): return selected_quantitites, unit @classmethod - def get_products(cls, related_object_type=None): + def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]: if related_object_type == "PRODUCT": - products = tool.Spatial.get_selected_products() + products = list(tool.Spatial.get_selected_products()) elif related_object_type == "PROCESS": products = [tool.Sequence.get_highlighted_task()] elif related_object_type == "RESOURCE": @@ -301,17 +306,17 @@ class Cost(blenderbim.core.tool.Cost): return products or [] @classmethod - def enable_editing_cost_item_quantities(cls, cost_item=None): + def enable_editing_cost_item_quantities(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.active_cost_item_id = cost_item.id() props.cost_item_editing_type = "QUANTITIES" @classmethod - def enable_editing_cost_item_quantity(cls, physical_quantity=None): + def enable_editing_cost_item_quantity(cls, physical_quantity: ifcopenshell.entity_instance) -> None: bpy.context.scene.BIMCostProperties.active_cost_item_quantity_id = physical_quantity.id() @classmethod - def load_cost_item_quantity_attributes(cls, physical_quantity=None): + def load_cost_item_quantity_attributes(cls, physical_quantity: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.quantity_attributes.clear() blenderbim.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes) @@ -323,16 +328,18 @@ class Cost(blenderbim.core.tool.Cost): props.cost_item_editing_type = "VALUES" @classmethod - def disable_editing_cost_item_quantity(cls): + def disable_editing_cost_item_quantity(cls) -> None: bpy.context.scene.BIMCostProperties.active_cost_item_quantity_id = 0 @classmethod - def get_cost_item_quantity_attributes(cls): + def get_cost_item_quantity_attributes(cls) -> dict[str, Any]: props = bpy.context.scene.BIMCostProperties return blenderbim.bim.helper.export_attributes(props.quantity_attributes) @classmethod - def get_attributes_for_cost_value(cls, cost_type, cost_category): + def get_attributes_for_cost_value( + cls, cost_type: Literal["FIXED", "SUM", "CATEGORY"], cost_category: Optional[str] = None + ) -> dict[str, Any]: if cost_type == "FIXED": category = None attributes = {"AppliedValue": bpy.context.scene.BIMCostProperties.fixed_cost_value} @@ -429,7 +436,7 @@ class Cost(blenderbim.core.tool.Cost): props.cost_value_editing_type = "FORMULA" @classmethod - def get_cost_item_value_formula(cls): + def get_cost_item_value_formula(cls) -> str: return bpy.context.scene.BIMCostProperties.cost_value_formula @classmethod @@ -469,7 +476,7 @@ class Cost(blenderbim.core.tool.Cost): ) @classmethod - def show_nested_cost_item_elements(cls): + def show_nested_cost_item_elements(cls) -> bool: return bpy.context.scene.BIMCostProperties.show_nested_elements @classmethod @@ -532,7 +539,7 @@ class Cost(blenderbim.core.tool.Cost): props.columns.remove(props.columns.find(name)) @classmethod - def expand_cost_item_rate(cls, cost_item): + def expand_cost_item_rate(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties contracted_cost_item_rates = json.loads(props.contracted_cost_item_rates) contracted_cost_item_rates.remove(cost_item) @@ -540,7 +547,7 @@ class Cost(blenderbim.core.tool.Cost): cls.load_schedule_of_rates_tree(schedule_of_rates=tool.Ifc.get().by_id(int(props.schedule_of_rates))) @classmethod - def contract_cost_item_rate(cls, cost_item): + def contract_cost_item_rate(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties contracted_cost_item_rates = json.loads(props.contracted_cost_item_rates) contracted_cost_item_rates.append(cost_item) @@ -571,7 +578,7 @@ class Cost(blenderbim.core.tool.Cost): ] @classmethod - def load_schedule_of_rates_tree(cls, schedule_of_rates): + def load_schedule_of_rates_tree(cls, schedule_of_rates: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.is_cost_update_enabled = False props.cost_item_rates.clear() @@ -585,7 +592,9 @@ class Cost(blenderbim.core.tool.Cost): props.is_cost_update_enabled = True @classmethod - def export_cost_schedules(cls, filepath: str, format: str, cost_schedule=None): + def export_cost_schedules( + cls, filepath: str, format: str, cost_schedule: Optional[ifcopenshell.entity_instance] = None + ) -> Union[str, None]: import subprocess import os import sys @@ -624,7 +633,7 @@ class Cost(blenderbim.core.tool.Cost): return "Could not open file location" @classmethod - def get_units(cls): + def get_units(cls) -> dict[int, str]: units = {} for unit in tool.Ifc.get().by_type("IfcNamedUnit"): if unit.get_info().get("UnitType", None) in [ @@ -639,7 +648,7 @@ class Cost(blenderbim.core.tool.Cost): return units @staticmethod - def format_unit(unit): + def format_unit(unit: ifcopenshell.entity_instance) -> str: if unit.is_a("IfcContextDependentUnit"): return f"{unit.UnitType} / {unit.Name}" else: @@ -649,7 +658,7 @@ class Cost(blenderbim.core.tool.Cost): return f"{unit.UnitType} / {name}" @classmethod - def get_cost_schedule(cls, cost_item): + def get_cost_schedule(cls, cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: for rel in cost_item.HasAssignments or []: if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcCostSchedule"): return rel.RelatingControl @@ -657,17 +666,17 @@ class Cost(blenderbim.core.tool.Cost): return cls.get_cost_schedule(rel.RelatingObject) @classmethod - def is_cost_schedule_active(cls, cost_schedule): + def is_cost_schedule_active(cls, cost_schedule: ifcopenshell.entity_instance) -> bool: return True if cost_schedule.id() == bpy.context.scene.BIMCostProperties.active_cost_schedule_id else False @classmethod - def get_active_cost_schedule(cls): + def get_active_cost_schedule(cls) -> Union[ifcopenshell.entity_instance, None]: if not bpy.context.scene.BIMCostProperties.active_cost_schedule_id: return None return tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id) @classmethod - def highlight_cost_item(cls, cost_item): + def highlight_cost_item(cls, cost_item: ifcopenshell.entity_instance) -> None: def expand_ancestors(cost_item): cls.expand_cost_item(cost_item) for rel in cost_item.Nests or []: @@ -685,11 +694,13 @@ class Cost(blenderbim.core.tool.Cost): bpy.context.scene.BIMCostProperties.active_cost_item_index = cost_item_index @classmethod - def get_cost_items_for_product(cls, product): + def get_cost_items_for_product(cls, product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.cost.get_cost_items_for_product(product) @classmethod - def has_cost_assignments(cls, product, cost_schedule=None): + def has_cost_assignments( + cls, product: ifcopenshell.entity_instance, cost_schedule: Optional[ifcopenshell.entity_instance] = None + ) -> bool: cost_items = ifcopenshell.util.cost.get_cost_items_for_product(product) if cost_schedule: cost_items = [ @@ -714,7 +725,7 @@ class Cost(blenderbim.core.tool.Cost): new.total_cost_quantity = ifcopenshell.util.cost.get_total_quantity(cost_item) @classmethod - def get_quantity_unit_symbol(cls, quantity): + def get_quantity_unit_symbol(cls, quantity: ifcopenshell.entity_instance) -> Union[str, None]: unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get()) if unit: return ifcopenshell.util.unit.get_unit_symbol(unit) @@ -722,14 +733,15 @@ class Cost(blenderbim.core.tool.Cost): return None @classmethod - def is_root_cost_item(cls, cost_item): + def is_root_cost_item(cls, cost_item: ifcopenshell.entity_instance) -> bool: if cost_item.HasAssignments: for rel in cost_item.HasAssignments: if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcCostSchedule"): return True + return False @classmethod - def toggle_cost_item_parent_change(cls, cost_item=None): + def toggle_cost_item_parent_change(cls, cost_item: Optional[ifcopenshell.entity_instance] = None) -> None: if not cost_item: return props = bpy.context.scene.BIMCostProperties @@ -740,16 +752,18 @@ class Cost(blenderbim.core.tool.Cost): cls.disable_editing_cost_item_parent() @classmethod - def change_parent_cost_item(cls, cost_item, new_parent): + def change_parent_cost_item( + cls, cost_item: ifcopenshell.entity_instance, new_parent: ifcopenshell.entity_instance + ) -> None: ifcopenshell.api.run("nest.change_nest", tool.Ifc.get(), item=cost_item, new_parent=new_parent) @classmethod - def disable_editing_cost_item_parent(cls): + def disable_editing_cost_item_parent(cls) -> None: bpy.context.scene.BIMCostProperties.active_cost_item_id = 0 bpy.context.scene.BIMCostProperties.change_cost_item_parent = False @classmethod - def load_cost_item_quantities(cls, cost_item=None): + def load_cost_item_quantities(cls, cost_item: Optional[ifcopenshell.entity_instance] = None) -> None: if not cost_item: cost_item = cls.get_highlighted_cost_item() if not cost_item: @@ -759,7 +773,9 @@ class Cost(blenderbim.core.tool.Cost): cls.load_cost_item_quantity_assignments(cost_item, related_object_type="RESOURCE") @classmethod - def update_cost_items(cls, product=None, pset=None): + def update_cost_items( + cls, product: Optional[ifcopenshell.entity_instance] = None, pset: Optional[ifcopenshell.entity_instance] = None + ) -> None: cost_items = [] if product: cost_items = ifcopenshell.util.cost.get_cost_items_for_product(product) @@ -779,11 +795,11 @@ class Cost(blenderbim.core.tool.Cost): cls.load_cost_item_quantity_assignments(cost_item, related_object_type="PRODUCT") @classmethod - def has_schedules(cls): + def has_schedules(cls) -> bool: return bool(tool.Ifc.get().by_type("IfcCostSchedule")) @classmethod - def get_currency_attributes(cls): + def get_currency_attributes(cls) -> dict[str, str]: props = bpy.context.scene.BIMCostProperties currency = props.currency if currency == "CUSTOM": diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py index ca08ab802a..a4f654623a 100755 --- a/src/ifcdiff/ifcdiff.py +++ b/src/ifcdiff/ifcdiff.py @@ -35,30 +35,29 @@ import ifcopenshell.util.classification import ifcopenshell.util.representation from deepdiff import DeepDiff from ordered_set import OrderedSet +from typing import Optional, Union, Literal, Any __version__ = version = "0.0.0" +RELATIONSHIP_TYPE = Literal["geometry", "attributes", "type", "property", "container", "aggregate", "classification"] + + class IfcDiff: """Main IfcDiff application If you are using IfcDiff as a library, this is the class you should use. :param old: IFC file object for the old model - :type old: ifcopenshell.file.file :param new: IFC file object for the new model - :type new: ifcopenshell.file.file :param relationships: List of relationships to check. None means that only - geometry is compared. - :type relationships: list[string] + geometry is compared. See RELATIONSHIP_TYPE for available relationships. :param is_shallow: True if you want only the first difference to be listed. False if you want all differences to be checked. Choosing False means that comparisons will take longer. - :type is_shallow: bool :param filter_elements: An IFC filter query if you only want to compare a subset of elements. For example: ``IfcWall`` to only compare walls. - :type filter_elements: string Example:: @@ -70,7 +69,19 @@ class IfcDiff: ifc_diff.export() """ - def __init__(self, old, new, relationships=None, is_shallow=True, filter_elements=None): + added_elements: set[ifcopenshell.entity_instance] + deleted_elements: set[ifcopenshell.entity_instance] + # GlobalIds to changes dictionary. + change_register: dict[str, dict[str, Any]] + + def __init__( + self, + old: ifcopenshell.file, + new: ifcopenshell.file, + relationships: Optional[list[RELATIONSHIP_TYPE]] = None, + is_shallow: bool = True, + filter_elements: Optional[str] = None, + ): self.old = old self.new = new self.change_register = {} @@ -80,7 +91,7 @@ class IfcDiff: self.is_shallow = is_shallow self.filter_elements = filter_elements - def diff(self): + def diff(self) -> None: logging.disable(logging.CRITICAL) self.precision = self.get_precision() @@ -187,7 +198,9 @@ class IfcDiff: logging.disable(logging.NOTSET) - def summarise_shapes(self, ifc, elements): + def summarise_shapes( + self, ifc: ifcopenshell.file, elements: list[ifcopenshell.entity_instance] + ) -> dict[str, dict[str, Any]]: shapes = {} iterator = ifcopenshell.geom.iterator( self.get_settings(ifc), ifc, multiprocessing.cpu_count(), include=elements @@ -215,7 +228,7 @@ class IfcDiff: break return shapes - def get_settings(self, ifc): + def get_settings(self, ifc: ifcopenshell.file) -> ifcopenshell.geom.settings: settings = ifcopenshell.geom.settings() # Are you feeling lucky? settings.set(settings.DISABLE_BOOLEAN_RESULT, True) @@ -246,7 +259,7 @@ class IfcDiff: return list(obj) return json.JSONEncoder.default(None, obj) - def export(self, path): + def export(self, path: str) -> None: with open(path, "w", encoding="utf-8") as diff_file: json.dump( { @@ -259,7 +272,7 @@ class IfcDiff: default=self.json_dump_default, ) - def get_precision(self): + def get_precision(self) -> float: contexts = [c for c in self.new.by_type("IfcGeometricRepresentationContext") if c.ContextType == "Model"] if contexts: return contexts[0].Precision or 1e-4 @@ -356,7 +369,7 @@ class IfcDiff: # self.representation_ids[new_rep_id] = result # return result - def diff_representation(self, old_rep_id, new_rep_id): + def diff_representation(self, old_rep_id: int, new_rep_id: int) -> bool: old_rep = self.old.by_id(old_rep_id) new_rep = self.new.by_id(new_rep_id) if len(old_rep.Items) != len(new_rep.Items): @@ -365,8 +378,11 @@ class IfcDiff: result = self.diff_representation_item(old_item, new_rep.Items[i]) if result is True: return True + return False - def diff_representation_item(self, old_item, new_item): + def diff_representation_item( + self, old_item: ifcopenshell.entity_instance, new_item: ifcopenshell.entity_instance + ) -> bool: if old_item.is_a() != new_item.is_a(): return True try: @@ -381,8 +397,9 @@ class IfcDiff: return True if diff: return True + return False - def get_representation_id(self, element): + def get_representation_id(self, element: ifcopenshell.entity_instance) -> Union[int, None]: if not element.Representation: return for representation in element.Representation.Representations: @@ -404,6 +421,7 @@ class DiffTerminator: def give_up_diffing(self, level, diff_instance) -> bool: if any(diff_instance.tree.values()): raise Exception("Terminated") + return False if __name__ == "__main__":