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
@@ -6,6 +6,7 @@ bpy = sys.modules.get("bpy")
if bpy is not None:
import bpy
import blenderbim.bim.module.root as module_root
import blenderbim.bim.module.attribute as module_attribute
import blenderbim.bim.module.bcf as module_bcf
import blenderbim.bim.module.context as module_context
import blenderbim.bim.module.covetool as module_covetool
@@ -87,8 +88,6 @@ if bpy is not None:
operator.UnassignDocumentReference,
operator.RemoveObjectDocumentReference,
operator.GenerateGlobalId,
operator.AddAttribute,
operator.RemoveAttribute,
operator.AddMaterialAttribute,
operator.RemoveMaterialAttribute,
operator.QuickProjectSetup,
@@ -312,6 +311,7 @@ if bpy is not None:
]
classes.extend(module_root.classes)
classes.extend(module_attribute.classes)
classes.extend(module_bcf.classes)
classes.extend(module_context.classes)
classes.extend(module_covetool.classes)
@@ -350,6 +350,7 @@ if bpy is not None:
bpy.types.TextCurve.BIMTextProperties = bpy.props.PointerProperty(type=prop.BIMTextProperties)
bpy.types.SCENE_PT_unit.append(ui.ifc_units)
module_root.register()
module_attribute.register()
module_bcf.register()
module_context.register()
module_covetool.register()
@@ -382,5 +383,6 @@ if bpy is not None:
module_covetool.unregister()
module_context.unregister()
module_bcf.unregister()
module_attribute.register()
module_root.unregister()
bpy.app.handlers.depsgraph_update_pre.remove(operator.depsgraph_update_pre_handler)
@@ -847,7 +847,6 @@ class IfcImporter:
obj = bpy.data.objects.new(self.get_name(element), mesh)
obj.BIMObjectProperties.ifc_definition_id = element.id()
self.material_creator.create(element, obj, mesh)
self.add_element_attributes(element, obj.BIMObjectProperties)
self.add_element_classifications(element, obj)
self.add_element_document_relations(element, obj)
self.add_type_product_psets(element, obj)
@@ -975,7 +974,6 @@ class IfcImporter:
obj.matrix_world = self.get_element_matrix(element)
self.add_element_representation_items(element, obj)
self.add_element_attributes(element, obj.BIMObjectProperties)
self.add_element_classifications(element, obj)
self.add_element_document_relations(element, obj)
self.add_defines_by_type_relation(element, obj)
@@ -1732,7 +1730,6 @@ class IfcImporter:
obj.instance_type = "COLLECTION"
obj.instance_collection = collection
self.place_object_in_spatial_tree(element, obj)
self.add_element_attributes(element, obj.BIMObjectProperties)
self.add_element_classifications(element, obj)
self.add_element_document_relations(element, obj)
self.add_defines_by_type_relation(element, obj)
@@ -1795,7 +1792,6 @@ class IfcImporter:
obj.BIMObjectProperties.ifc_definition_id = element.id()
self.material_creator.create(element, obj, mesh)
obj.matrix_world = self.get_element_matrix(element, mesh_name)
self.add_element_attributes(element, obj.BIMObjectProperties)
self.add_element_classifications(element, obj)
self.add_element_document_relations(element, obj)
self.add_defines_by_type_relation(element, obj)
@@ -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")
@@ -13,5 +13,5 @@ class Data:
cls.products[product_id] = {
"type": product.is_a(),
"PredefinedType": product.PredefinedType if hasattr(product, "PredefinedType") else None,
"ObjectType": product.ObjectType or None
"ObjectType": product.ObjectType if hasattr(product, "ObjectType") else None
}
@@ -10,7 +10,9 @@ class Data:
if not file:
return
product = file.by_id(product_id)
if product.ContainedInStructure:
if not hasattr(product, "ContainedInStructure"):
cls.products[product_id] = None
elif product.ContainedInStructure:
structure = product.ContainedInStructure[0].RelatingStructure
cls.products[product_id] = {"type": structure.is_a(), "Name": structure.Name, "id": int(structure.id())}
else:
@@ -11,7 +11,14 @@ class BIM_PT_spatial(Panel):
@classmethod
def poll(cls, context):
return bool(context.active_object.BIMObjectProperties.ifc_definition_id)
props = context.active_object.BIMObjectProperties
if not props.ifc_definition_id:
return False
if props.ifc_definition_id not in Data.products:
Data.load(props.ifc_definition_id)
if not Data.products[props.ifc_definition_id]:
return False
return True
def draw(self, context):
props = context.active_object.BIMObjectProperties
@@ -20,9 +27,6 @@ class BIM_PT_spatial(Panel):
if props.ifc_definition_id not in Data.products:
Data.load(props.ifc_definition_id)
if "Ifc" not in context.active_object.name:
return
row = self.layout.row(align=True)
name = "{}/{}".format(
Data.products[props.ifc_definition_id]["type"], Data.products[props.ifc_definition_id]["Name"]
@@ -910,28 +910,6 @@ class GenerateGlobalId(bpy.types.Operator):
return {"FINISHED"}
class AddAttribute(bpy.types.Operator):
bl_idname = "bim.add_attribute"
bl_label = "Add Attribute"
def execute(self, context):
if not bpy.context.active_object.BIMObjectProperties.applicable_attributes:
return {"FINISHED"}
name = bpy.context.active_object.BIMObjectProperties.applicable_attributes
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(bpy.context.scene.BIMProperties.export_schema)
for obj in bpy.context.selected_objects:
if "/" not in obj.name or obj.BIMObjectProperties.attributes.find(name) != -1:
continue
entity = schema.declaration_by_name(obj.name.split("/")[0])
if name not in [a.name() for a in entity.all_attributes()]:
continue
attribute = obj.BIMObjectProperties.attributes.add()
attribute.name = name
if attribute.name == "GlobalId":
attribute.string_value = ifcopenshell.guid.new()
return {"FINISHED"}
class AddMaterialAttribute(bpy.types.Operator):
bl_idname = "bim.add_material_attribute"
bl_label = "Add Material Attribute"
@@ -943,22 +921,6 @@ class AddMaterialAttribute(bpy.types.Operator):
return {"FINISHED"}
class RemoveAttribute(bpy.types.Operator):
bl_idname = "bim.remove_attribute"
bl_label = "Remove Attribute"
attribute_index: bpy.props.IntProperty()
def execute(self, context):
name = bpy.context.active_object.BIMObjectProperties.attributes[self.attribute_index].name
for obj in bpy.context.selected_objects:
if "/" not in obj.name:
continue
index = obj.BIMObjectProperties.attributes.find(name)
if index != -1:
obj.BIMObjectProperties.attributes.remove(index)
return {"FINISHED"}
class RemoveMaterialAttribute(bpy.types.Operator):
bl_idname = "bim.remove_material_attribute"
bl_label = "Remove Material Attribute"
+8 -15
View File
@@ -274,6 +274,10 @@ def getIfcClasses(self, context):
return classes_enum
def getAttributeEnumValues(self, context):
return [(e, e, "") for e in json.loads(self.enum_items)]
def getProfileDef(self, context):
global profiledef_enum
if len(profiledef_enum) < 1:
@@ -440,20 +444,6 @@ def getQtoNames(self, context):
return qtonames_enum
def getApplicableAttributes(self, context):
global attributes_enum
attributes_enum.clear()
if "/" in context.active_object.name and context.active_object.name.split("/")[0] in schema.ifc.elements:
attributes_enum.extend(
[
(a["name"], a["name"], "")
for a in schema.ifc.elements[context.active_object.name.split("/")[0]]["attributes"]
if self.attributes.find(a["name"]) == -1
]
)
return attributes_enum
def getApplicableMaterialAttributes(self, context):
global materialattributes_enum
materialattributes_enum.clear()
@@ -548,6 +538,9 @@ class Attribute(PropertyGroup):
bool_value: BoolProperty(name="Value")
int_value: IntProperty(name="Value")
float_value: FloatProperty(name="Value")
is_null: BoolProperty(name="Is Null")
enum_items: StringProperty(name="Value")
enum_value: EnumProperty(items=getAttributeEnumValues, name="Value")
class MaterialLayer(PropertyGroup):
@@ -1467,11 +1460,11 @@ class BIMObjectProperties(PropertyGroup):
is_reassigning_class: BoolProperty(name="Is Reassigning Class")
global_ids: CollectionProperty(name="GlobalIds", type=GlobalId)
attributes: CollectionProperty(name="Attributes", type=Attribute)
is_editing_attributes: BoolProperty(name="Is Editing Attributes")
relating_type: PointerProperty(name="Type Product", type=bpy.types.Object)
relating_structure: PointerProperty(name="Spatial Container", type=bpy.types.Object)
psets: CollectionProperty(name="Psets", type=PsetQto)
qtos: CollectionProperty(name="Qtos", type=PsetQto)
applicable_attributes: EnumProperty(items=getApplicableAttributes, name="Attribute Names")
document_references: CollectionProperty(name="Document References", type=DocumentReference)
active_document_reference_index: IntProperty(name="Active Document Reference Index")
constraints: CollectionProperty(name="Constraints", type=Constraint)
-49
View File
@@ -26,59 +26,10 @@ class BIM_PT_object(Panel):
if "Ifc" not in context.active_object.name:
return
layout.label(text="Attributes:")
row = layout.row(align=True)
row.prop(props, "applicable_attributes", text="")
row.operator("bim.add_attribute")
for index, attribute in enumerate(props.attributes):
row = layout.row(align=True)
row.prop(attribute, "name", text="")
row.prop(attribute, "string_value", text="")
if attribute.name == "GlobalId":
row.operator("bim.generate_global_id", icon="FILE_REFRESH", text="")
op = row.operator("bim.copy_attribute_to_selection", icon="COPYDOWN", text="")
op.attribute_name = attribute.name
op.attribute_value = attribute.string_value
row.operator("bim.remove_attribute", icon="X", text="").attribute_index = index
row = layout.row()
row.prop(props, "attributes")
if "IfcSite/" in context.active_object.name or "IfcBuilding/" in context.active_object.name:
self.draw_addresses_ui()
row = layout.row(align=True)
row.prop(props, "relating_type")
row.operator("bim.select_similar_type", icon="RESTRICT_SELECT_OFF", text="")
def draw_addresses_ui(self):
layout = self.layout
layout.label(text="Address:")
address = bpy.context.active_object.BIMObjectProperties.address
row = layout.row()
row.prop(address, "purpose")
if address.purpose == "USERDEFINED":
row = layout.row()
row.prop(address, "user_defined_purpose")
row = layout.row()
row.prop(address, "description")
row = layout.row()
row.prop(address, "internal_location")
row = layout.row()
row.prop(address, "address_lines")
row = layout.row()
row.prop(address, "postal_box")
row = layout.row()
row.prop(address, "town")
row = layout.row()
row.prop(address, "region")
row = layout.row()
row.prop(address, "postal_code")
row = layout.row()
row.prop(address, "country")
class BIM_PT_object_material(Panel):
bl_label = "IFC Object Material"