WIP migrate attribute management to partial editing with 4 new features! See #1222.

New features include:

 - New clarity between null and empty string attributes
 - Attribute editing now shows appropriate UI widgets depending on data type
 - Attribute editing for enumerations now show a dropdown
 - Attribute editing is now schema sensitive
This commit is contained in:
Dion Moult
2021-01-05 13:16:31 +11:00
parent d4a219e54f
commit 30b3090386
13 changed files with 292 additions and 114 deletions
@@ -0,0 +1,17 @@
import bpy
from . import ui, operator
classes = (
operator.EnableEditingAttributes,
operator.DisableEditingAttributes,
operator.EditAttributes,
ui.BIM_PT_attributes,
)
def register():
pass
def unregister():
pass
@@ -0,0 +1,58 @@
import ifcopenshell
from blenderbim.bim.ifc import IfcStore
class Data:
products = {}
@classmethod
def load(cls, product_id):
file = IfcStore.get_file()
if not file:
return
product = file.by_id(product_id)
cls.products[product_id] = []
attributes = product.get_info()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema)
declaration = schema.declaration_by_name(product.is_a())
for attribute in declaration.all_attributes():
data_type = str(attribute.type_of_attribute())
value = getattr(product, attribute.name())
list_type = None
enum_items = ()
if "<entity" in data_type:
data_type = "entity"
value = None if value is None else str(value)
elif "<list" in data_type:
if "<entity" in data_type:
list_type = "entity"
elif "<string>" in data_type:
list_type = "string"
elif "<real>" in data_type:
list_type = "float"
elif "<integer>" in data_type:
list_type = "integer"
data_type = "list"
value = None if value is None else str(value)
elif "<string>" in data_type:
data_type = "string"
value = None if value is None else str(value)
elif "<real>" in data_type:
data_type = "float"
value = None if value is None else float(value)
elif "<integer>" in data_type:
data_type = "integer"
value = None if value is None else int(value)
elif "<enumeration" in data_type:
data_type = "enum"
value = None if value is None else str(value)
enum_items = attribute.type_of_attribute().declared_type().enumeration_items()
cls.products[product_id].append({
"name": attribute.name(),
"value": value,
"type": data_type,
"enum_items": enum_items,
"list_type": list_type,
"is_optional": attribute.optional(),
"is_null": getattr(product, attribute.name()) is None
})
@@ -0,0 +1,13 @@
class Usecase():
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"product": None,
"attributes": {}
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
for name, value in self.settings["attributes"].items():
setattr(self.settings["product"], name, value)
@@ -0,0 +1,88 @@
import bpy
import json
import blenderbim.bim.module.attribute.edit_attributes as edit_attributes
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.attribute.data import Data
class EnableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.enable_editing_attributes"
bl_label = "Enable Editing Attributes"
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.context.active_object
props = obj.BIMObjectProperties
while len(props.attributes) > 0:
props.attributes.remove(0)
for attribute in Data.products[props.ifc_definition_id]:
new = props.attributes.add()
if attribute["type"] == "entity":
continue
new.name = attribute["name"]
new.is_null = attribute["is_null"]
if attribute["type"] == "string" or attribute["type"] == "list":
new.string_value = attribute["value"] or ""
elif attribute["type"] == "integer":
new.int_value = attribute["value"] or 0
elif attribute["type"] == "float":
new.float_value = attribute["value"] or 0.
elif attribute["type"] == "enum":
new.enum_items = json.dumps(attribute["enum_items"])
if attribute["value"]:
new.enum_value = attribute["value"]
props.is_editing_attributes = True
return {"FINISHED"}
class DisableEditingAttributes(bpy.types.Operator):
bl_idname = "bim.disable_editing_attributes"
bl_label = "Disable Editing Attributes"
def execute(self, context):
obj = bpy.context.active_object
props = obj.BIMObjectProperties
props.is_editing_attributes = False
return {"FINISHED"}
class EditAttributes(bpy.types.Operator):
bl_idname = "bim.edit_attributes"
bl_label = "Edit Attributes"
def execute(self, context):
self.file = IfcStore.get_file()
obj = bpy.context.active_object
props = obj.BIMObjectProperties
attributes = {}
for attribute in Data.products[props.ifc_definition_id]:
blender_attribute = props.attributes.get(attribute["name"])
if not blender_attribute:
continue
if blender_attribute.is_null:
attributes[attribute["name"]] = None
elif attribute["type"] == "string":
attributes[attribute["name"]] = blender_attribute.string_value
elif attribute["type"] == "list":
values = blender_attribute.string_value[1:-1].split(", ")
print(attribute["name"])
print(attribute["list_type"])
if attribute["list_type"] == "float":
values = [float(v) for v in values]
elif attribute["list_type"] == "integer":
values = [int(v) for v in values]
attributes[attribute["name"]] = values
elif attribute["type"] == "integer":
attributes[attribute["name"]] = blender_attribute.int_value
elif attribute["type"] == "float":
attributes[attribute["name"]] = blender_attribute.float_value
elif attribute["type"] == "enum":
attributes[attribute["name"]] = blender_attribute.enum_value
usecase = edit_attributes.Usecase(self.file, {
"product": self.file.by_id(props.ifc_definition_id),
"attributes": attributes
})
usecase.execute()
Data.load(props.ifc_definition_id)
bpy.ops.bim.disable_editing_attributes()
return {"FINISHED"}
@@ -0,0 +1,92 @@
from bpy.types import Panel
from blenderbim.bim.module.attribute.data import Data
class BIM_PT_attributes(Panel):
bl_label = "IFC Attributes"
bl_idname = "BIM_PT_attributes"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
@classmethod
def poll(cls, context):
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
def draw(self, context):
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return
if props.ifc_definition_id not in Data.products:
Data.load(props.ifc_definition_id)
props = context.active_object.BIMObjectProperties
if props.is_editing_attributes:
row = self.layout.row(align=True)
row.operator("bim.edit_attributes", icon="CHECKMARK", text="Save Attributes")
row.operator("bim.disable_editing_attributes", icon="X", text="")
for attribute in Data.products[props.ifc_definition_id]:
if attribute["type"] == "entity":
continue
row = self.layout.row(align=True)
blender_attribute = props.attributes.get(attribute["name"])
if attribute["type"] == "string" or attribute["type"] == "list":
row.prop(blender_attribute, "string_value", text=attribute["name"])
elif attribute["type"] == "integer":
row.prop(blender_attribute, "int_value", text=attribute["name"])
elif attribute["type"] == "float":
row.prop(blender_attribute, "float_value", text=attribute["name"])
elif attribute["type"] == "enum":
row.prop(blender_attribute, "enum_value", text=attribute["name"])
if attribute["name"] == "GlobalId":
row.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
if attribute["is_optional"]:
row.prop(blender_attribute, "is_null", icon="RADIOBUT_OFF" if blender_attribute.is_null else "RADIOBUT_ON", text="")
# TODO: reimplement, see #1222
#op = row.operator("bim.copy_attribute_to_selection", icon="COPYDOWN", text="")
#op.attribute_name = attribute.name
#op.attribute_value = attribute.string_value
else:
row = self.layout.row()
row.operator("bim.enable_editing_attributes", icon="GREASEPENCIL", text="Edit")
for attribute in Data.products[props.ifc_definition_id]:
if attribute["value"] is None or attribute["type"] == "entity":
continue
row = self.layout.row(align=True)
row.label(text=attribute["name"])
row.label(text=str(attribute["value"]))
# TODO: reimplement, see #1222
#if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
# self.draw_addresses_ui()
def draw_addresses_ui(self):
self.layout.label(text="Address:")
address = bpy.context.active_object.BIMObjectProperties.address
row = self.layout.row()
row.prop(address, "purpose")
if address.purpose == "USERDEFINED":
row = self.layout.row()
row.prop(address, "user_defined_purpose")
row = self.layout.row()
row.prop(address, "description")
row = self.layout.row()
row.prop(address, "internal_location")
row = self.layout.row()
row.prop(address, "address_lines")
row = self.layout.row()
row.prop(address, "postal_box")
row = self.layout.row()
row.prop(address, "town")
row = self.layout.row()
row.prop(address, "region")
row = self.layout.row()
row.prop(address, "postal_code")
row = self.layout.row()
row.prop(address, "country")