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 e7a63d2a0b.

* Place Patch description in recipe enum field

* Minor fixes

* Minor fixes

* Remove description field from Attribute

* Auto-update IFC Patch args on recipe change
This commit is contained in:
Gorgious56
2021-08-16 00:16:51 +02:00
committed by GitHub
parent d22c11c200
commit 296bf0387d
20 changed files with 247 additions and 325 deletions
+14 -29
View File
@@ -11,22 +11,8 @@ from blenderbim.bim.ifc import IfcStore
def draw_attributes(props, layout, copy_operator=None): def draw_attributes(props, layout, copy_operator=None):
for attribute in props: for attribute in props:
row = layout.row(align=True) row = layout.row(align=True)
value = None draw_attribute(attribute, row)
if attribute.data_type == "string": value = attribute.get_value()
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
if attribute.is_optional: if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
if copy_operator: 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}) 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): def import_attributes(ifc_class, props, data, callback=None):
for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes(): for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes():
data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) 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 is_handled_by_callback = callback(attributes, prop) if callback else False
if is_handled_by_callback: if is_handled_by_callback:
continue # Our job is done continue # Our job is done
attributes[prop.name] = prop.get_value()
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
return attributes return attributes
@@ -1,5 +1,6 @@
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from ifcopenshell.api.constraint.data import Data from ifcopenshell.api.constraint.data import Data
@@ -44,14 +45,7 @@ class BIM_PT_constraints(Panel):
self.draw_editable_ui(context) self.draw_editable_ui(context)
def draw_editable_ui(self, context): def draw_editable_ui(self, context):
for attribute in self.props.constraint_attributes: draw_attributes(self.props.constraint_attributes, self.layout)
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="")
class BIM_PT_object_constraints(Panel): class BIM_PT_object_constraints(Panel):
@@ -1,6 +1,7 @@
import blenderbim.bim.module.cost.prop as CostProp import blenderbim.bim.module.cost.prop as CostProp
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from ifcopenshell.api.cost.data import Data 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") self.layout.template_list("BIM_UL_cost_columns", "", self.props, "columns", self.props, "active_column_index")
def draw_editable_cost_schedule_ui(self): def draw_editable_cost_schedule_ui(self):
for attribute in self.props.cost_schedule_attributes: draw_attributes(self.props.cost_schedule_attributes, self.layout)
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="")
def draw_editable_cost_item_ui(self, cost_schedule_id): def draw_editable_cost_item_ui(self, cost_schedule_id):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -119,18 +113,7 @@ class BIM_PT_cost_schedules(Panel):
self.draw_editable_cost_item_values_ui() self.draw_editable_cost_item_values_ui()
def draw_editable_cost_item_attributes_ui(self): def draw_editable_cost_item_attributes_ui(self):
for attribute in self.props.cost_item_attributes: draw_attributes(self.props.cost_item_attributes, self.layout)
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="")
def draw_editable_cost_item_quantities_ui(self): def draw_editable_cost_item_quantities_ui(self):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -165,20 +148,7 @@ class BIM_PT_cost_schedules(Panel):
self.draw_editable_cost_item_quantity_ui(box) self.draw_editable_cost_item_quantity_ui(box)
def draw_editable_cost_item_quantity_ui(self, layout): def draw_editable_cost_item_quantity_ui(self, layout):
for attribute in self.props.quantity_attributes: draw_attributes(self.props.quantity_attributes, self.layout)
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="")
def draw_editable_cost_item_values_ui(self): def draw_editable_cost_item_values_ui(self):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -253,21 +223,7 @@ class BIM_PT_cost_schedules(Panel):
op.cost_value = cost_value_id op.cost_value = cost_value_id
def draw_editable_cost_value_ui(self, layout, cost_value): def draw_editable_cost_value_ui(self, layout, cost_value):
for attribute in self.props.cost_value_attributes: draw_attributes(self.props.cost_value_attributes, self.layout)
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="")
class BIM_PT_cost_item_quantities(Panel): class BIM_PT_cost_item_quantities(Panel):
bl_label = "IFC Cost Item Quantities" bl_label = "IFC Cost Item Quantities"
@@ -1,5 +1,6 @@
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from ifcopenshell.api.document.data import Data from ifcopenshell.api.document.data import Data
@@ -48,14 +49,7 @@ class BIM_PT_documents(Panel):
self.draw_editable_ui(context) self.draw_editable_ui(context)
def draw_editable_ui(self, context): def draw_editable_ui(self, context):
for attribute in self.props.document_attributes: draw_attributes(self.props.document_attributes, self.layout)
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="")
class BIM_PT_object_documents(Panel): class BIM_PT_object_documents(Panel):
@@ -102,16 +102,7 @@ class EditGeoreferencing(bpy.types.Operator):
if data_type == "entity": if data_type == "entity":
continue continue
blender_attribute = props.projected_crs.get(attribute.name()) blender_attribute = props.projected_crs.get(attribute.name())
if blender_attribute.is_null: projected_crs[attribute.name()] = blender_attribute.get_value()
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
map_unit = "" map_unit = ""
if not props.is_map_unit_null: if not props.is_map_unit_null:
@@ -2,6 +2,7 @@ import ifcopenshell.util.geolocation
from bpy.types import Panel from bpy.types import Panel
from ifcopenshell.api.georeference.data import Data from ifcopenshell.api.georeference.data import Data
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes, draw_attribute
class BIM_PT_gis(Panel): 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.edit_georeferencing", icon="CHECKMARK", text="")
row.operator("bim.disable_editing_georeferencing", icon="X", text="") row.operator("bim.disable_editing_georeferencing", icon="X", text="")
for attribute in props.projected_crs: draw_attributes(props.projected_crs, self.layout)
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="")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.prop(props, "map_unit_type", text="MapUnit") row.prop(props, "map_unit_type", text="MapUnit")
@@ -62,17 +53,7 @@ class BIM_PT_gis(Panel):
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.operator("bim.set_ifc_grid_north", text="Set IFC North") row.operator("bim.set_ifc_grid_north", text="Set IFC North")
row.operator("bim.set_blender_grid_north", text="Set Blender North") row.operator("bim.set_blender_grid_north", text="Set Blender North")
row = self.layout.row(align=True) draw_attribute(attribute, self.layout.row())
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="")
row = self.layout.row() row = self.layout.row()
row.label(text="True North", icon="LIGHT_SUN") row.label(text="True North", icon="LIGHT_SUN")
@@ -655,17 +655,7 @@ class EditMaterialSetItem(bpy.types.Operator):
props = obj.BIMObjectMaterialProperties props = obj.BIMObjectMaterialProperties
product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id] product_data = Data.products[obj.BIMObjectProperties.ifc_definition_id]
attributes = {} attributes = {attribute.name: attribute.get_value() for attribute in props.material_set_item_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
if product_data["type"] == "IfcMaterialConstituentSet": if product_data["type"] == "IfcMaterialConstituentSet":
ifcopenshell.api.run( ifcopenshell.api.run(
@@ -690,19 +680,7 @@ class EditMaterialSetItem(bpy.types.Operator):
) )
Data.load_layers() Data.load_layers()
elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage": elif product_data["type"] == "IfcMaterialProfileSet" or product_data["type"] == "IfcMaterialProfileSetUsage":
profile_attributes = {} profile_attributes = {attr.name: attr.get_value() for attr in props.material_set_item_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
ifcopenshell.api.run( ifcopenshell.api.run(
"material.edit_profile", "material.edit_profile",
self.file, self.file,
@@ -3,6 +3,7 @@ from bpy.types import Panel
from ifcopenshell.api.material.data import Data from ifcopenshell.api.material.data import Data
from ifcopenshell.api.profile.data import Data as ProfileData from ifcopenshell.api.profile.data import Data as ProfileData
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
class BIM_PT_material(Panel): class BIM_PT_material(Panel):
@@ -181,17 +182,7 @@ class BIM_PT_object_material(Panel):
op.material_set_item = set_item_id op.material_set_item = set_item_id
row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="") row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
for attribute in self.props.material_set_item_attributes: draw_attributes(self.props.material_set_item_attributes, self.layout)
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="")
if self.set_item_name == "profile": if self.set_item_name == "profile":
self.draw_assign_profile_ui(box, item) 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="") row.operator("bim.disable_editing_material_set_item", icon="CANCEL", text="")
def draw_editable_profile_ui(self, layout, item): def draw_editable_profile_ui(self, layout, item):
for attribute in self.props.material_set_item_profile_attributes: draw_attributes(self.props.material_set_item_profile_attributes, self.layout)
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="")
def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False): def draw_read_only_set_item_ui(self, set_item_id, index, is_first=False, is_last=False):
if self.product_data["type"] == "IfcMaterialList": if self.product_data["type"] == "IfcMaterialList":
@@ -5,6 +5,7 @@ classes = (
operator.SelectIfcPatchInput, operator.SelectIfcPatchInput,
operator.SelectIfcPatchOutput, operator.SelectIfcPatchOutput,
operator.ExecuteIfcPatch, operator.ExecuteIfcPatch,
operator.UpdateIfcPatchArguments,
prop.BIMPatchProperties, prop.BIMPatchProperties,
ui.BIM_PT_patch, ui.BIM_PT_patch,
) )
@@ -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
@@ -1,6 +1,8 @@
import os import os
import bpy import bpy
import json import json
import ifcpatch
from .helper import extract_docs
class SelectIfcPatchInput(bpy.types.Operator): class SelectIfcPatchInput(bpy.types.Operator):
@@ -39,6 +41,7 @@ class ExecuteIfcPatch(bpy.types.Operator):
bl_idname = "bim.execute_ifc_patch" bl_idname = "bim.execute_ifc_patch"
bl_label = "Execute IFCPatch" bl_label = "Execute IFCPatch"
file_format: bpy.props.StringProperty() file_format: bpy.props.StringProperty()
use_json_for_args: bpy.props.BoolProperty()
@classmethod @classmethod
def poll(cls, context): 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] return os.path.isfile(input_file) and "ifc" in os.path.splitext(input_file)[1]
def execute(self, context): 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( ifcpatch.execute(
{ {
"input": context.scene.BIMPatchProperties.ifc_patch_input, "input": props.ifc_patch_input,
"output": context.scene.BIMPatchProperties.ifc_patch_output, "output": props.ifc_patch_output,
"recipe": context.scene.BIMPatchProperties.ifc_patch_recipes, "recipe": props.ifc_patch_recipes,
"arguments": json.loads(context.scene.BIMPatchProperties.ifc_patch_args or "[]"), "arguments": arguments,
"log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"), "log": os.path.join(context.scene.BIMProperties.data_dir, "process.log"),
} }
) )
return {"FINISHED"} 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"}
@@ -1,6 +1,7 @@
import bpy import bpy
import importlib import importlib
from pathlib import Path from pathlib import Path
import ifcpatch
from blenderbim.bim.prop import StrProperty, Attribute from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
@@ -13,6 +14,8 @@ from bpy.props import (
FloatVectorProperty, FloatVectorProperty,
CollectionProperty, CollectionProperty,
) )
from .helper import extract_docs
from .operator import UpdateIfcPatchArguments
ifcpatchrecipes_enum = [] ifcpatchrecipes_enum = []
@@ -32,12 +35,17 @@ def getIfcPatchRecipes(self, context):
f = str(filename.stem) f = str(filename.stem)
if f == "__init__": if f == "__init__":
continue 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 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): 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_input: StringProperty(default="", name="IFC Patch Input IFC")
ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC") ifc_patch_output: StringProperty(default="", name="IFC Patch Output IFC")
ifc_patch_args: StringProperty(default="", name="Arguments") ifc_patch_args: StringProperty(default="", name="Arguments")
ifc_patch_args_attr: CollectionProperty(type=Attribute, name="Arguments")
@@ -1,8 +1,8 @@
import bpy 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_label = "IFC Patch"
bl_idname = "BIM_PT_patch" bl_idname = "BIM_PT_patch"
bl_options = {"DEFAULT_CLOSED"} bl_options = {"DEFAULT_CLOSED"}
@@ -13,20 +13,25 @@ class BIM_PT_patch(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene scene = context.scene
props = scene.BIMPatchProperties props = scene.BIMPatchProperties
row = layout.row() row = layout.row()
row.prop(props, "ifc_patch_recipes") row.prop(props, "ifc_patch_recipes")
row = layout.row(align=True) row = layout.row(align=True)
row.prop(props, "ifc_patch_input") row.prop(props, "ifc_patch_input")
row.operator("bim.select_ifc_patch_input", icon="FILE_FOLDER", text="") row.operator("bim.select_ifc_patch_input", icon="FILE_FOLDER", text="")
row = layout.row(align=True) row = layout.row(align=True)
row.prop(props, "ifc_patch_output") row.prop(props, "ifc_patch_output")
row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="") row.operator("bim.select_ifc_patch_output", icon="FILE_FOLDER", text="")
row = layout.row()
row.prop(props, "ifc_patch_args")
row = layout.row() if props.ifc_patch_args_attr:
op = row.operator("bim.execute_ifc_patch") 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
@@ -136,31 +136,11 @@ class EnablePsetEditing(bpy.types.Operator):
prop = Data.properties[prop_id] prop = Data.properties[prop_id]
value = prop["NominalValue"] 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 = self.props.properties.add()
new.set_value(value)
new.name = prop["Name"] new.name = prop["Name"]
new.is_null = prop["NominalValue"] is None new.is_null = value is None
new.data_type = data_type new.set_value(new.get_value_default() if new.is_null else value)
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
class DisablePsetEditing(bpy.types.Operator): class DisablePsetEditing(bpy.types.Operator):
@@ -200,18 +180,7 @@ class EditPset(bpy.types.Operator):
else: else:
data = Data.psets if pset_id in Data.psets else Data.qtos data = Data.psets if pset_id in Data.psets else Data.qtos
for prop in props.properties: for prop in props.properties:
if prop.is_null: properties[prop.name] = prop.get_value()
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
if pset_id in Data.psets: if pset_id in Data.psets:
ifcopenshell.api.run( ifcopenshell.api.run(
@@ -1,6 +1,7 @@
from bpy.types import Panel from bpy.types import Panel
from ifcopenshell.api.pset.data import Data from ifcopenshell.api.pset.data import Data
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attribute
def get_active_pset_obj_name(context, obj_type): 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): def draw_psetqto_editable_ui(box, props, prop):
row = box.row(align=True) row = box.row()
if prop.data_type == "string": draw_attribute(prop, row)
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="")
if ( if (
"length" in prop.name.lower() "length" in prop.name.lower()
or "width" in prop.name.lower() or "width" in prop.name.lower()
@@ -2,6 +2,7 @@ import isodate
import blenderbim.bim.helper import blenderbim.bim.helper
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from ifcopenshell.api.sequence.data import Data from ifcopenshell.api.sequence.data import Data
from ifcopenshell.api.resource.data import Data as ResourceData from ifcopenshell.api.resource.data import Data as ResourceData
import blenderbim.bim.module.sequence.helper as helper import blenderbim.bim.module.sequence.helper as helper
@@ -55,14 +56,7 @@ class BIM_PT_work_plans(Panel):
self.draw_work_schedule_ui() self.draw_work_schedule_ui()
def draw_editable_ui(self): def draw_editable_ui(self):
for attribute in self.props.work_plan_attributes: draw_attributes(self.props.work_plan_attributes, self.layout)
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="")
def draw_work_schedule_ui(self): def draw_work_schedule_ui(self):
row = self.layout.row(align=True) row = self.layout.row(align=True)
@@ -189,14 +183,7 @@ class BIM_PT_work_schedules(Panel):
row.prop(self.props, "speed_multiplier", text="") row.prop(self.props, "speed_multiplier", text="")
def draw_editable_work_schedule_ui(self): def draw_editable_work_schedule_ui(self):
for attribute in self.props.work_schedule_attributes: draw_attributes(self.props.work_schedule_attributes, self.layout)
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="")
def draw_editable_task_ui(self, work_schedule_id): def draw_editable_task_ui(self, work_schedule_id):
self.layout.template_list( self.layout.template_list(
@@ -523,15 +510,7 @@ class BIM_PT_work_calendars(Panel):
self.draw_editable_work_time_ui(work_time) self.draw_editable_work_time_ui(work_time)
def draw_editable_work_time_ui(self, work_time): def draw_editable_work_time_ui(self, work_time):
for attribute in self.props.work_time_attributes: draw_attributes(self.props.work_time_attributes, self.layout)
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="")
if work_time["RecurrencePattern"]: if work_time["RecurrencePattern"]:
self.draw_editable_recurrence_pattern_ui(Data.recurrence_patterns[work_time["RecurrencePattern"]]) self.draw_editable_recurrence_pattern_ui(Data.recurrence_patterns[work_time["RecurrencePattern"]])
else: else:
@@ -599,11 +578,4 @@ class BIM_PT_work_calendars(Panel):
row.prop(self.props, "occurrences") row.prop(self.props, "occurrences")
def draw_editable_ui(self): def draw_editable_ui(self):
for attribute in self.props.work_calendar_attributes: draw_attributes(self.props.work_calendar_attributes, self.layout)
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="")
@@ -279,15 +279,7 @@ class EditStructuralAnalysisModel(bpy.types.Operator):
def _execute(self, context): def _execute(self, context):
props = context.scene.BIMStructuralProperties props = context.scene.BIMStructuralProperties
attributes = {} attributes = {attribute.name: attribute.get_value() for attribute in props.structural_analysis_model_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
self.file = IfcStore.get_file() self.file = IfcStore.get_file()
ifcopenshell.api.run( ifcopenshell.api.run(
"structural.edit_structural_analysis_model", "structural.edit_structural_analysis_model",
@@ -2,6 +2,7 @@ import bpy
import blenderbim.bim.helper import blenderbim.bim.helper
from bpy.types import Panel, UIList from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from ifcopenshell.api.structural.data import Data from ifcopenshell.api.structural.data import Data
@@ -293,14 +294,7 @@ class BIM_PT_structural_analysis_models(Panel):
self.draw_editable_ui(context) self.draw_editable_ui(context)
def draw_editable_ui(self, context): def draw_editable_ui(self, context):
for attribute in self.props.structural_analysis_model_attributes: draw_attributes(self.props.structural_analysis_model_attributes, self.layout)
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="")
class BIM_UL_structural_analysis_models(UIList): 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) self.draw_editable_load_case_group_ui(load_case)
def draw_editable_load_case_ui(self): def draw_editable_load_case_ui(self):
for attribute in self.props.load_case_attributes: draw_attributes(self.props.load_case_attributes, self.layout)
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="")
def draw_editable_load_case_group_ui(self, load_case): def draw_editable_load_case_group_ui(self, load_case):
box = self.layout.box() box = self.layout.box()
+50 -27
View File
@@ -126,43 +126,66 @@ class StrProperty(PropertyGroup):
pass pass
def updateAttributeStringValue(self, context): def updateAttributeValue(self, context):
updateAttributeValue(self, self.string_value) if getattr(self, str(self.get_value_attr()), None): # Do not use get_value since it returns None if is_null is True
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:
self.is_null = False self.is_null = False
class Attribute(PropertyGroup): class Attribute(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
data_type: StringProperty(name="Data Type") data_type: StringProperty(name="Data Type")
string_value: StringProperty(name="Value", update=updateAttributeStringValue) string_value: StringProperty(name="Value", update=updateAttributeValue)
bool_value: BoolProperty(name="Value", update=updateAttributeBoolValue) bool_value: BoolProperty(name="Value", update=updateAttributeValue)
int_value: IntProperty(name="Value", update=updateAttributeIntValue) int_value: IntProperty(name="Value", update=updateAttributeValue)
float_value: FloatProperty(name="Value", update=updateAttributeFloatValue) float_value: FloatProperty(name="Value", update=updateAttributeValue)
is_null: BoolProperty(name="Is Null") is_null: BoolProperty(name="Is Null")
is_optional: BoolProperty(name="Is Optional") is_optional: BoolProperty(name="Is Optional")
enum_items: StringProperty(name="Value") 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): class BIMProperties(PropertyGroup):
schema_dir: StringProperty( schema_dir: StringProperty(
+5 -1
View File
@@ -32,7 +32,11 @@ def execute(args, is_library=None):
print("# Loading patch recipe ...") print("# Loading patch recipe ...")
recipes = getattr(__import__("ifcpatch.recipes.{}".format(args["recipe"])), "recipes") recipes = getattr(__import__("ifcpatch.recipes.{}".format(args["recipe"])), "recipes")
recipe = getattr(recipes, args["recipe"]) 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 ...") print("# Patching ...")
patcher.patch() patcher.patch()
ifc_file = getattr(patcher, "file_patched", patcher.file) ifc_file = getattr(patcher, "file_patched", patcher.file)