From 296bf0387db1abb9417824c3f3d28efa5eb83b73 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Mon, 16 Aug 2021 00:16:51 +0200 Subject: [PATCH] Improve ifc patch arguments User Experience (#1655) * Add description field to Attribute Container * Add helper function to extract IFCPatch arguments * Add Operator to populate Attributes with IFCPatch arguments * Support int, float, bool and improve UX/UI * Move all reflection logic inside helper module * Improve UI * Correctly unpack arguments when Patching IFC * Refactor Attribute class to provide agnostic value access * Take advantage of Attribute agnosticity refactor * Use regular json if no docstring when Patching * centralize docstring extraction in UI module * Revert "centralize docstring extraction in UI module" This reverts commit e7a63d2a0ba709eb5d64552ea76722b58085438c. * Place Patch description in recipe enum field * Minor fixes * Minor fixes * Remove description field from Attribute * Auto-update IFC Patch args on recipe change --- src/blenderbim/blenderbim/bim/helper.py | 43 ++++------ .../blenderbim/bim/module/constraint/ui.py | 10 +-- .../blenderbim/bim/module/cost/ui.py | 54 ++----------- .../blenderbim/bim/module/document/ui.py | 10 +-- .../bim/module/georeference/operator.py | 11 +-- .../blenderbim/bim/module/georeference/ui.py | 25 +----- .../bim/module/material/operator.py | 26 +----- .../blenderbim/bim/module/material/ui.py | 28 +------ .../blenderbim/bim/module/patch/__init__.py | 1 + .../blenderbim/bim/module/patch/helper.py | 80 +++++++++++++++++++ .../blenderbim/bim/module/patch/operator.py | 45 +++++++++-- .../blenderbim/bim/module/patch/prop.py | 12 ++- .../blenderbim/bim/module/patch/ui.py | 21 +++-- .../blenderbim/bim/module/pset/operator.py | 39 +-------- .../blenderbim/bim/module/pset/ui.py | 15 +--- .../blenderbim/bim/module/sequence/ui.py | 38 ++------- .../bim/module/structural/operator.py | 10 +-- .../blenderbim/bim/module/structural/ui.py | 21 +---- src/blenderbim/blenderbim/bim/prop.py | 77 +++++++++++------- src/ifcpatch/ifcpatch/__init__.py | 6 +- 20 files changed, 247 insertions(+), 325 deletions(-) create mode 100644 src/blenderbim/blenderbim/bim/module/patch/helper.py diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index fefb377c45..8eef8750c1 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -11,22 +11,8 @@ from blenderbim.bim.ifc import IfcStore def draw_attributes(props, layout, copy_operator=None): for attribute in props: row = layout.row(align=True) - value = None - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - value = attribute.string_value - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - value = attribute.bool_value - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - value = attribute.int_value - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - value = attribute.float_value - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - value = attribute.enum_value + draw_attribute(attribute, row) + value = attribute.get_value() if attribute.is_optional: row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") if copy_operator: @@ -34,6 +20,17 @@ def draw_attributes(props, layout, copy_operator=None): op.data = json.dumps({"name": attribute.name, "value": value, "is_null": attribute.is_null}) +def draw_attribute(attribute, layout): + if not attribute.get_value_attr(): + layout.label(text=attribute.name) + else: + layout.prop( + attribute, + attribute.get_value_attr(), + text=attribute.name, + ) + + def import_attributes(ifc_class, props, data, callback=None): for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes(): data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) @@ -70,17 +67,5 @@ def export_attributes(props, callback=None): is_handled_by_callback = callback(attributes, prop) if callback else False if is_handled_by_callback: continue # Our job is done - - if prop.is_null: - attributes[prop.name] = None - elif prop.data_type == "string": - attributes[prop.name] = prop.string_value - elif prop.data_type == "boolean": - attributes[prop.name] = prop.bool_value - elif prop.data_type == "integer": - attributes[prop.name] = prop.int_value - elif prop.data_type == "float": - attributes[prop.name] = prop.float_value - elif prop.data_type == "enum": - attributes[prop.name] = prop.enum_value + attributes[prop.name] = prop.get_value() return attributes diff --git a/src/blenderbim/blenderbim/bim/module/constraint/ui.py b/src/blenderbim/blenderbim/bim/module/constraint/ui.py index 2a0ae2d6cd..22b09f092a 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/ui.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.constraint.data import Data @@ -44,14 +45,7 @@ class BIM_PT_constraints(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.constraint_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.constraint_attributes, self.layout) class BIM_PT_object_constraints(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 952dabd44d..779d329d4f 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -1,6 +1,7 @@ import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.cost.data import Data @@ -66,14 +67,7 @@ class BIM_PT_cost_schedules(Panel): self.layout.template_list("BIM_UL_cost_columns", "", self.props, "columns", self.props, "active_column_index") def draw_editable_cost_schedule_ui(self): - for attribute in self.props.cost_schedule_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.cost_schedule_attributes, self.layout) def draw_editable_cost_item_ui(self, cost_schedule_id): row = self.layout.row(align=True) @@ -119,18 +113,7 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_values_ui() def draw_editable_cost_item_attributes_ui(self): - for attribute in self.props.cost_item_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.cost_item_attributes, self.layout) def draw_editable_cost_item_quantities_ui(self): row = self.layout.row(align=True) @@ -165,20 +148,7 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_quantity_ui(box) def draw_editable_cost_item_quantity_ui(self, layout): - for attribute in self.props.quantity_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.quantity_attributes, self.layout) def draw_editable_cost_item_values_ui(self): row = self.layout.row(align=True) @@ -253,21 +223,7 @@ class BIM_PT_cost_schedules(Panel): op.cost_value = cost_value_id def draw_editable_cost_value_ui(self, layout, cost_value): - for attribute in self.props.cost_value_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - + draw_attributes(self.props.cost_value_attributes, self.layout) class BIM_PT_cost_item_quantities(Panel): bl_label = "IFC Cost Item Quantities" diff --git a/src/blenderbim/blenderbim/bim/module/document/ui.py b/src/blenderbim/blenderbim/bim/module/document/ui.py index 0bbafafb77..70e9edcce5 100644 --- a/src/blenderbim/blenderbim/bim/module/document/ui.py +++ b/src/blenderbim/blenderbim/bim/module/document/ui.py @@ -1,5 +1,6 @@ from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.document.data import Data @@ -48,14 +49,7 @@ class BIM_PT_documents(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.document_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.document_attributes, self.layout) class BIM_PT_object_documents(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index 22a883db5b..ff1970152b 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -102,16 +102,7 @@ class EditGeoreferencing(bpy.types.Operator): if data_type == "entity": continue blender_attribute = props.projected_crs.get(attribute.name()) - if blender_attribute.is_null: - projected_crs[attribute.name()] = None - elif blender_attribute.data_type == "string": - projected_crs[attribute.name()] = blender_attribute.string_value - elif blender_attribute.data_type == "float": - projected_crs[attribute.name()] = blender_attribute.float_value - elif blender_attribute.data_type == "integer": - projected_crs[attribute.name()] = blender_attribute.int_value - elif blender_attribute.data_type == "boolean": - projected_crs[attribute.name()] = blender_attribute.bool_value + projected_crs[attribute.name()] = blender_attribute.get_value() map_unit = "" if not props.is_map_unit_null: diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index 19f843541a..8b5cbc4f50 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -2,6 +2,7 @@ import ifcopenshell.util.geolocation from bpy.types import Panel from ifcopenshell.api.georeference.data import Data from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes, draw_attribute class BIM_PT_gis(Panel): @@ -34,17 +35,7 @@ class BIM_PT_gis(Panel): row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="") row.operator("bim.disable_editing_georeferencing", icon="X", text="") - for attribute in props.projected_crs: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(props.projected_crs, self.layout) row = self.layout.row(align=True) row.prop(props, "map_unit_type", text="MapUnit") @@ -62,17 +53,7 @@ class BIM_PT_gis(Panel): row = self.layout.row(align=True) row.operator("bim.set_ifc_grid_north", text="Set IFC North") row.operator("bim.set_blender_grid_north", text="Set Blender North") - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attribute(attribute, self.layout.row()) row = self.layout.row() row.label(text="True North", icon="LIGHT_SUN") diff --git a/src/blenderbim/blenderbim/bim/module/material/operator.py b/src/blenderbim/blenderbim/bim/module/material/operator.py index 0d071dedbc..1fde8c931a 100644 --- a/src/blenderbim/blenderbim/bim/module/material/operator.py +++ b/src/blenderbim/blenderbim/bim/module/material/operator.py @@ -655,17 +655,7 @@ class EditMaterialSetItem(bpy.types.Operator): props = obj.BIMObjectMaterialProperties product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] - attributes = {} - for attribute in props.material_set_item_attributes: - if attribute.data_type == "string": - value = attribute.string_value - elif attribute.data_type == "float": - value = attribute.float_value - elif attribute.data_type == "integer": - value = attribute.int_value - elif attribute.data_type == "boolean": - value = attribute.bool_value - attributes[attribute.name] = None if attribute.is_null else value + attributes = {attribute.name: attribute.get_value() for attribute in props.material_set_item_attributes} if product_data["type"] == "IfcMaterialConstituentSet": ifcopenshell.api.run( @@ -690,19 +680,7 @@ class EditMaterialSetItem(bpy.types.Operator): ) Data.load_layers() elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage": - profile_attributes = {} - for attribute in props.material_set_item_profile_attributes: - if attribute.data_type == "string": - value = attribute.string_value - elif attribute.data_type == "float": - value = attribute.float_value - elif attribute.data_type == "integer": - value = attribute.int_value - elif attribute.data_type == "boolean": - value = attribute.bool_value - elif attribute.data_type == "enum": - value = attribute.enum_value - profile_attributes[attribute.name] = None if attribute.is_null else value + profile_attributes = {attr.name: attr.get_value() for attr in props.material_set_item_profile_attributes} ifcopenshell.api.run( "material.edit_profile", self.file, diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index 1a803aa836..e531eb754c 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -3,6 +3,7 @@ from bpy.types import Panel from ifcopenshell.api.material.data import Data from ifcopenshell.api.profile.data import Data as ProfileData from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes class BIM_PT_material(Panel): @@ -181,17 +182,7 @@ class BIM_PT_object_material(Panel): op.material_set_item = set_item_id row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="") - for attribute in self.props.material_set_item_attributes: - row = box.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.material_set_item_attributes, self.layout) if self.set_item_name == "profile": self.draw_assign_profile_ui(box, item) @@ -213,20 +204,7 @@ class BIM_PT_object_material(Panel): row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="") def draw_editable_profile_ui(self, layout, item): - for attribute in self.props.material_set_item_profile_attributes: - row = layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "integer": - row.prop(attribute, "int_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "boolean": - row.prop(attribute, "bool_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.material_set_item_profile_attributes, self.layout) def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): if self.product_data["type"] == "IfcMaterialList": diff --git a/src/blenderbim/blenderbim/bim/module/patch/__init__.py b/src/blenderbim/blenderbim/bim/module/patch/__init__.py index d8a39535d2..1770b7cf78 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/patch/__init__.py @@ -5,6 +5,7 @@ classes = ( operator.SelectIfcPatchInput, operator.SelectIfcPatchOutput, operator.ExecuteIfcPatch, + operator.UpdateIfcPatchArguments, prop.BIMPatchProperties, ui.BIM_PT_patch, ) diff --git a/src/blenderbim/blenderbim/bim/module/patch/helper.py b/src/blenderbim/blenderbim/bim/module/patch/helper.py new file mode 100644 index 0000000000..f5712065b1 --- /dev/null +++ b/src/blenderbim/blenderbim/bim/module/patch/helper.py @@ -0,0 +1,80 @@ +import typing +import inspect +import collections +import importlib +from types import ModuleType + +def extract_docs( + module: ModuleType, + submodule_name: str, + cls_name: str, + method_name: str, + boilerplate_args : typing.Iterable[str]=None): + """Extract class docstrings and method arguments + + :param module: Parent module from which to extract the submodule class + :param submodule_name: Submodule from which to extract the class + :param cls_name: Class from which to extract the docstring and method arguments + :param method_name: Class Method name from which to extract arguments + :param boilerplate_args: String iterable containing arguments that shall not be parsed + """ + spec = importlib.util.spec_from_file_location(submodule_name, f"{module.__path__[0]}/recipes/{submodule_name}.py") + submodule = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(submodule) + try: + return _extract_docs(getattr(submodule, cls_name), method_name, boilerplate_args) + except AttributeError as e: + print(e) + except ModuleNotFoundError as e: + print(f"Error : IFCPatch {str(submodule)} could not complete because : {str(e)}") + +def _extract_docs(cls, method_name, boilerplate_args): + inputs = collections.OrderedDict() + method = getattr(cls, method_name) + node_data = {"class": cls} + + signature = inspect.signature(method) + for name, parameter in signature.parameters.items(): + if name == "self": + continue + inputs[name] = {"name": name} + if isinstance(parameter.default, (str, float, int, bool)): + inputs[name]["default"] = parameter.default + + type_hints = typing.get_type_hints(method) + for name, socket_data in inputs.items(): + type_hint = type_hints.get(name, None) + if type_hint is None: # The argument is not type-hinted. (Or hinted to None ??) + continue + if isinstance(type_hint, typing._UnionGenericAlias): + inputs[name]["type"] = [t.__name__ for t in typing.get_args(type_hint)] + else: + inputs[name]["type"] = type_hint.__name__ + + description = "" + doc = method.__doc__ + if doc is not None: + for i, line in enumerate(doc.split("\n")): + line = line.strip() + if i == 0: + node_data["name"] = line + elif line.startswith(":return:"): + node_data["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()} + elif line.startswith(":param"): + param_name = line.split(":")[1].strip().replace("param ", "") + if param_name in inputs: + inputs[param_name]["description"] = line.split(":")[2].strip() + elif i == 2: + description += line + elif i > 2: + description += "\n" + line + + node_data["description"] = description.strip() + node_data["inputs"] = inputs + + if boilerplate_args is not None: + for arg in boilerplate_args: # Remove boilerplate arguments + node_data["inputs"].pop(arg, None) + return node_data + diff --git a/src/blenderbim/blenderbim/bim/module/patch/operator.py b/src/blenderbim/blenderbim/bim/module/patch/operator.py index 5015942931..5abf2b3a3b 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/operator.py +++ b/src/blenderbim/blenderbim/bim/module/patch/operator.py @@ -1,6 +1,8 @@ import os import bpy import json +import ifcpatch +from .helper import extract_docs class SelectIfcPatchInput(bpy.types.Operator): @@ -39,6 +41,7 @@ class ExecuteIfcPatch(bpy.types.Operator): bl_idname = "bim.execute_ifc_patch" bl_label = "Execute IFCPatch" file_format: bpy.props.StringProperty() + use_json_for_args: bpy.props.BoolProperty() @classmethod def poll(cls, context): @@ -46,15 +49,47 @@ class ExecuteIfcPatch(bpy.types.Operator): return os.path.isfile(input_file) and "ifc" in os.path.splitext(input_file)[1] def execute(self, context): - import ifcpatch + props = context.scene.BIMPatchProperties + if self.use_json_for_args or not props.ifc_patch_args_attr: + arguments = json.loads(props.ifc_patch_args or "[]") + else: + arguments = [arg.get_value() for arg in props.ifc_patch_args_attr] ifcpatch.execute( { - "input": context.scene.BIMPatchProperties.ifc_patch_input, - "output": context.scene.BIMPatchProperties.ifc_patch_output, - "recipe": context.scene.BIMPatchProperties.ifc_patch_recipes, - "arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"), + "input": props.ifc_patch_input, + "output": props.ifc_patch_output, + "recipe": props.ifc_patch_recipes, + "arguments": arguments, "log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"), } ) return {"FINISHED"} + + +class UpdateIfcPatchArguments(bpy.types.Operator): + bl_idname = "bim.update_ifc_patch_arguments" + bl_label = "Update IFC Patch arguments" + recipe: bpy.props.StringProperty() + + def execute(self, context): + if self.recipe == "": + print("No Recipe Selected. Impossible to load arguments") + return {"FINISHED"} + patch_args = context.scene.BIMPatchProperties.ifc_patch_args_attr + patch_args.clear() + docs = extract_docs(ifcpatch, self.recipe, "Patcher", "__init__", ("src", "file", "logger", "args")) + if docs and "inputs" in docs: + inputs = docs["inputs"] + for arg_name in inputs: + arg_info = inputs[arg_name] + new_attr = patch_args.add() + new_attr.data_type = { + "str": "string", + "float": "float", + "int": "integer", + "bool": "boolean", + }[arg_info.get("type", "str")] + new_attr.name = arg_name + new_attr.set_value(arg_info.get("default", new_attr.get_value_default())) + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/patch/prop.py b/src/blenderbim/blenderbim/bim/module/patch/prop.py index bfee66f4b0..86608eb6e0 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/prop.py +++ b/src/blenderbim/blenderbim/bim/module/patch/prop.py @@ -1,6 +1,7 @@ import bpy import importlib from pathlib import Path +import ifcpatch from blenderbim.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -13,6 +14,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from .helper import extract_docs +from .operator import UpdateIfcPatchArguments ifcpatchrecipes_enum = [] @@ -32,12 +35,17 @@ def getIfcPatchRecipes(self, context): f = str(filename.stem) if f == "__init__": continue - ifcpatchrecipes_enum.append((f, f, "")) + docs = extract_docs(ifcpatch, f, "Patcher", "__init__", ("src", "file", "logger", "args")) + ifcpatchrecipes_enum.append((f, f, docs.get("description","") if docs else "")) return ifcpatchrecipes_enum +def update_ifc_patch_recipe(self, context): + bpy.ops.bim.update_ifc_patch_arguments(recipe = self.ifc_patch_recipes) + class BIMPatchProperties(PropertyGroup): - ifc_patch_recipes: EnumProperty(items=getIfcPatchRecipes, name="Recipes") + ifc_patch_recipes: EnumProperty(items=getIfcPatchRecipes, name="Recipes", update=update_ifc_patch_recipe) ifc_patch_input: StringProperty(default="", name="IFC Patch Input IFC") ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC") ifc_patch_args: StringProperty(default="", name="Arguments") + ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments") diff --git a/src/blenderbim/blenderbim/bim/module/patch/ui.py b/src/blenderbim/blenderbim/bim/module/patch/ui.py index 49600d2d1c..80c3fab2c2 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/ui.py +++ b/src/blenderbim/blenderbim/bim/module/patch/ui.py @@ -1,8 +1,8 @@ import bpy -from bpy.types import Panel +from blenderbim.bim.helper import draw_attributes -class BIM_PT_patch(Panel): +class BIM_PT_patch(bpy.types.Panel): bl_label = "IFC Patch" bl_idname = "BIM_PT_patch" bl_options = {"DEFAULT_CLOSED"} @@ -13,20 +13,25 @@ class BIM_PT_patch(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True + layout.use_property_decorate = False scene = context.scene props = scene.BIMPatchProperties - row = layout.row() - row.prop(props, "ifc_patch_recipes") + row.prop(props, "ifc_patch_recipes") + + row = layout.row(align=True) row.prop(props, "ifc_patch_input") row.operator("bim.select_ifc_patch_input", icon="FILE_FOLDER", text="") + row = layout.row(align=True) row.prop(props, "ifc_patch_output") row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="") - row = layout.row() - row.prop(props, "ifc_patch_args") - row = layout.row() - op = row.operator("bim.execute_ifc_patch") + if props.ifc_patch_args_attr: + draw_attributes(props.ifc_patch_args_attr, layout) + else: + layout.row().prop(props, "ifc_patch_args") + op = layout.operator("bim.execute_ifc_patch") + op.use_json_for_args = len(props.ifc_patch_args_attr) == 0 diff --git a/src/blenderbim/blenderbim/bim/module/pset/operator.py b/src/blenderbim/blenderbim/bim/module/pset/operator.py index 071d5dab3b..b34586c14b 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/operator.py +++ b/src/blenderbim/blenderbim/bim/module/pset/operator.py @@ -136,31 +136,11 @@ class EnablePsetEditing(bpy.types.Operator): prop = Data.properties[prop_id] value = prop["NominalValue"] - if isinstance(value, str): - data_type = "string" - elif isinstance(value, float): - data_type = "float" - elif isinstance(value, bool): - data_type = "boolean" - elif isinstance(value, int): - data_type = "integer" - else: - data_type = "string" - value = str(value) - new = self.props.properties.add() + new.set_value(value) new.name = prop["Name"] - new.is_null = prop["NominalValue"] is None - new.data_type = data_type - - if data_type == "string": - new.string_value = "" if new.is_null else value - elif data_type == "integer": - new.int_value = 0 if new.is_null else value - elif data_type == "float": - new.float_value = 0.0 if new.is_null else value - elif data_type == "boolean": - new.bool_value = False if new.is_null else value + new.is_null = value is None + new.set_value(new.get_value_default() if new.is_null else value) class DisablePsetEditing(bpy.types.Operator): @@ -200,18 +180,7 @@ class EditPset(bpy.types.Operator): else: data = Data.psets if pset_id in Data.psets else Data.qtos for prop in props.properties: - if prop.is_null: - properties[prop.name] = None - elif prop.data_type == "string": - properties[prop.name] = prop.string_value - elif prop.data_type == "boolean": - properties[prop.name] = prop.bool_value - elif prop.data_type == "integer": - properties[prop.name] = prop.int_value - elif prop.data_type == "float": - properties[prop.name] = prop.float_value - elif prop.data_type == "enum": - properties[prop.name] = prop.enum_value + properties[prop.name] = prop.get_value() if pset_id in Data.psets: ifcopenshell.api.run( diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 4bed5d65dc..53fae14dda 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -1,6 +1,7 @@ from bpy.types import Panel from ifcopenshell.api.pset.data import Data from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attribute def get_active_pset_obj_name(context, obj_type): @@ -67,18 +68,8 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type): def draw_psetqto_editable_ui(box, props, prop): - row = box.row(align=True) - if prop.data_type == "string": - row.prop(prop, "string_value", text=prop.name) - elif prop.data_type == "integer": - row.prop(prop, "int_value", text=prop.name) - elif prop.data_type == "float": - row.prop(prop, "float_value", text=prop.name) - elif prop.data_type == "boolean": - row.prop(prop, "bool_value", text=prop.name) - elif prop.data_type == "enum": - row.prop(prop, "enum_value", text=prop.name) - row.prop(prop, "is_null", icon="RADIOBUT_OFF" if prop.is_null else "RADIOBUT_ON", text="") + row = box.row() + draw_attribute(prop, row) if ( "length" in prop.name.lower() or "width" in prop.name.lower() diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 79083e4060..d294c8865b 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -2,6 +2,7 @@ import isodate import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.sequence.data import Data from ifcopenshell.api.resource.data import Data as ResourceData import blenderbim.bim.module.sequence.helper as helper @@ -55,14 +56,7 @@ class BIM_PT_work_plans(Panel): self.draw_work_schedule_ui() def draw_editable_ui(self): - for attribute in self.props.work_plan_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_plan_attributes, self.layout) def draw_work_schedule_ui(self): row = self.layout.row(align=True) @@ -189,14 +183,7 @@ class BIM_PT_work_schedules(Panel): row.prop(self.props, "speed_multiplier", text="") def draw_editable_work_schedule_ui(self): - for attribute in self.props.work_schedule_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_schedule_attributes, self.layout) def draw_editable_task_ui(self, work_schedule_id): self.layout.template_list( @@ -523,15 +510,7 @@ class BIM_PT_work_calendars(Panel): self.draw_editable_work_time_ui(work_time) def draw_editable_work_time_ui(self, work_time): - for attribute in self.props.work_time_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") - + draw_attributes(self.props.work_time_attributes, self.layout) if work_time["RecurrencePattern"]: self.draw_editable_recurrence_pattern_ui(Data.recurrence_patterns[work_time["RecurrencePattern"]]) else: @@ -599,11 +578,4 @@ class BIM_PT_work_calendars(Panel): row.prop(self.props, "occurrences") def draw_editable_ui(self): - for attribute in self.props.work_calendar_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.work_calendar_attributes, self.layout) diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py index 6b036bccb0..38133c4abc 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/operator.py +++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py @@ -279,15 +279,7 @@ class EditStructuralAnalysisModel(bpy.types.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - attributes = {} - for attribute in props.structural_analysis_model_attributes: - if attribute.is_null: - attributes[attribute.name] = None - else: - if attribute.data_type == "string": - attributes[attribute.name] = attribute.string_value - elif attribute.data_type == "enum": - attributes[attribute.name] = attribute.enum_value + attributes = {attribute.name: attribute.get_value() for attribute in props.structural_analysis_model_attributes} self.file = IfcStore.get_file() ifcopenshell.api.run( "structural.edit_structural_analysis_model", diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index 5ce31355b8..6e64fd7ea8 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -2,6 +2,7 @@ import bpy import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore +from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.structural.data import Data @@ -293,14 +294,7 @@ class BIM_PT_structural_analysis_models(Panel): self.draw_editable_ui(context) def draw_editable_ui(self, context): - for attribute in self.props.structural_analysis_model_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.structural_analysis_model_attributes, self.layout) class BIM_UL_structural_analysis_models(UIList): @@ -389,16 +383,7 @@ class BIM_PT_structural_load_cases(Panel): self.draw_editable_load_case_group_ui(load_case) def draw_editable_load_case_ui(self): - for attribute in self.props.load_case_attributes: - row = self.layout.row(align=True) - if attribute.data_type == "string": - row.prop(attribute, "string_value", text=attribute.name) - elif attribute.data_type == "float": - row.prop(attribute, "float_value", text=attribute.name) - elif attribute.data_type == "enum": - row.prop(attribute, "enum_value", text=attribute.name) - if attribute.is_optional: - row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") + draw_attributes(self.props.load_case_attributes, self.layout) def draw_editable_load_case_group_ui(self, load_case): box = self.layout.box() diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 4e71c57112..0e891b744f 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -126,43 +126,66 @@ class StrProperty(PropertyGroup): pass -def updateAttributeStringValue(self, context): - updateAttributeValue(self, self.string_value) - - -def updateAttributeBoolValue(self, context): - updateAttributeValue(self, self.bool_value) - - -def updateAttributeIntValue(self, context): - updateAttributeValue(self, self.int_value) - - -def updateAttributeFloatValue(self, context): - updateAttributeValue(self, self.float_value) - - -def updateAttributeEnumValue(self, context): - updateAttributeValue(self, self.enum_value) - - -def updateAttributeValue(self, value): - if value: +def updateAttributeValue(self, context): + if getattr(self, str(self.get_value_attr()), None): # Do not use get_value since it returns None if is_null is True self.is_null = False class Attribute(PropertyGroup): name: StringProperty(name="Name") data_type: StringProperty(name="Data Type") - string_value: StringProperty(name="Value", update=updateAttributeStringValue) - bool_value: BoolProperty(name="Value", update=updateAttributeBoolValue) - int_value: IntProperty(name="Value", update=updateAttributeIntValue) - float_value: FloatProperty(name="Value", update=updateAttributeFloatValue) + string_value: StringProperty(name="Value", update=updateAttributeValue) + bool_value: BoolProperty(name="Value", update=updateAttributeValue) + int_value: IntProperty(name="Value", update=updateAttributeValue) + float_value: FloatProperty(name="Value", update=updateAttributeValue) is_null: BoolProperty(name="Is Null") is_optional: BoolProperty(name="Is Optional") enum_items: StringProperty(name="Value") - enum_value: EnumProperty(items=getAttributeEnumValues, name="Value", update=updateAttributeEnumValue) + enum_value: EnumProperty(items=getAttributeEnumValues, name="Value", update=updateAttributeValue) + def get_value(self): + if self.is_null: + return None + return getattr(self, str(self.get_value_attr()), None) + + def get_value_default(self): + if self.data_type == "string": + return "" + elif self.data_type == "integer": + return 0 + elif self.data_type == "float": + return 0.0 + elif self.data_type == "boolean": + return False + elif self.data_type == "enum": + return "0" + + def get_value_attr(self): + if self.data_type == "string": + return "string_value" + elif self.data_type == "boolean": + return "bool_value" + elif self.data_type == "integer": + return "int_value" + elif self.data_type == "float": + return "float_value" + elif self.data_type == "enum": + return "enum_value" + + def set_value(self, value): + if isinstance(value, str): + self.data_type = "string" + elif isinstance(value, float): + self.data_type = "float" + elif isinstance(value, bool): # Make sure this is evaluated BEFORE integer + self.data_type = "boolean" + elif isinstance(value, int): + self.data_type = "integer" + else: + self.data_type = "string" + value = str(value) + setattr(self, self.get_value_attr(), value) + class BIMProperties(PropertyGroup): schema_dir: StringProperty( diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 8c7b4b161e..6c2b60a369 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -32,7 +32,11 @@ def execute(args, is_library=None): print("# Loading patch recipe ...") recipes = getattr(__import__("ifcpatch.recipes.{}".format(args["recipe"])), "recipes") recipe = getattr(recipes, args["recipe"]) - patcher = recipe.Patcher(args["input"], ifc_file, logger, args["arguments"]) + try: + # We unpack the arguments if the Patcher has been type-hinted and docstringed + patcher = recipe.Patcher(args["input"], ifc_file, logger, *args["arguments"]) + except TypeError: + patcher = recipe.Patcher(args["input"], ifc_file, logger, args["arguments"]) print("# Patching ...") patcher.patch() ifc_file = getattr(patcher, "file_patched", patcher.file)