mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user