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):
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
@@ -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):
@@ -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"
@@ -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):
@@ -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:
@@ -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")
@@ -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,
@@ -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":
@@ -5,6 +5,7 @@ classes = (
operator.SelectIfcPatchInput,
operator.SelectIfcPatchOutput,
operator.ExecuteIfcPatch,
operator.UpdateIfcPatchArguments,
prop.BIMPatchProperties,
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 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"}
@@ -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")
@@ -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
@@ -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(
@@ -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()
@@ -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)
@@ -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",
@@ -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()
+50 -27
View File
@@ -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(
+5 -1
View File
@@ -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)