WIP port classification module with new features. See #1222.

* Adaptive UI based on IFC version
 * You can now load new classification libraries on the fly
 * You can mix classification libraries with project references
This commit is contained in:
Dion Moult
2021-01-21 21:58:27 +11:00
parent 460cb85e4b
commit 73439e2114
15 changed files with 726 additions and 326 deletions
@@ -0,0 +1,35 @@
import bpy
from . import ui, prop, operator
classes = (
operator.LoadClassificationLibrary,
operator.AddClassification,
operator.RemoveClassification,
operator.EnableEditingClassification,
operator.DisableEditingClassification,
operator.EditClassification,
operator.RemoveClassificationReference,
operator.EnableEditingClassificationReference,
operator.DisableEditingClassificationReference,
operator.EditClassificationReference,
operator.AddClassificationReference,
operator.ChangeClassificationLevel,
prop.ClassificationReference,
prop.BIMClassificationProperties,
prop.BIMClassificationReferenceProperties,
ui.BIM_PT_classifications,
ui.BIM_PT_classification_references,
ui.BIM_UL_classifications,
)
def register():
bpy.types.Scene.BIMClassificationProperties = bpy.props.PointerProperty(type=prop.BIMClassificationProperties)
bpy.types.Object.BIMClassificationReferenceProperties = bpy.props.PointerProperty(
type=prop.BIMClassificationReferenceProperties
)
def unregister():
del bpy.types.Scene.BIMClassificationProperties
del bpy.types.Object.BIMClassificationReferenceProperties
@@ -0,0 +1,26 @@
import ifcopenshell
import ifcopenshell.util.schema
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"classification": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
migrator = ifcopenshell.util.schema.Migrator()
result = migrator.migrate(self.settings["classification"], self.file)
self.file.create_entity("IfcRelAssociatesClassification", **{
"GlobalId": ifcopenshell.guid.new(),
"RelatedObjects": [self.file.by_type("IfcProject")[0]],
"RelatingClassification": result
})
return # See bug #1272
try:
result = self.file.add(self.settings["classification"])
except:
migrator = ifcopenshell.util.schema.Migrator()
result = migrator.migrate(self.settings["classification"], self.file)
@@ -0,0 +1,53 @@
import ifcopenshell
import ifcopenshell.util.schema
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"product": None,
"reference": None,
"classification": None,
}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
relating_classification = None
for reference in self.file.by_type("IfcClassificationReference"):
if self.file.schema == "IFC2X3":
if reference.ItemReference == self.settings["reference"].ItemReference:
relating_classification = reference
break
else:
if reference.Identification == self.settings["reference"].Identification:
relating_classification = reference
break
if relating_classification:
association = self.get_association(relating_classification)
related_objects = set(association.RelatedObjects)
related_objects.add(self.settings["product"])
association.RelatedObjects = list(related_objects)
return
migrator = ifcopenshell.util.schema.Migrator()
# This removal patch is to support a lightweight classification
old_referenced_source = self.settings["reference"].ReferencedSource
self.settings["reference"].ReferencedSource = None
relating_classification = migrator.migrate(self.settings["reference"], self.file)
relating_classification.ReferencedSource = self.settings["classification"]
self.settings["reference"].ReferencedSource = old_referenced_source
self.file.create_entity("IfcRelAssociatesClassification", **{
"GlobalId": ifcopenshell.guid.new(),
"RelatedObjects": [self.settings["product"]],
"RelatingClassification": relating_classification
})
def get_association(self, reference):
if self.file.schema == "IFC2X3":
for association in self.file.by_type("IfcRelAssociatesClassification"):
if relating_classification == reference:
return association
elif reference.ClassificationRefForObjects:
return reference.ClassificationRefForObjects[0]
@@ -0,0 +1,71 @@
import ifcopenshell
from blenderbim.bim.ifc import IfcStore
from datetime import datetime
class Data:
is_loaded = False
products = {}
classifications = {}
references = {}
library_file = None
library_classifications = {}
library_references = {}
@classmethod
def load(cls, product_id=None):
cls._file = IfcStore.get_file()
if not cls._file:
return
if product_id:
return cls.load_product_classifications(product_id)
cls.load_classifications()
cls.load_references()
cls.is_loaded = True
@classmethod
def load_product_classifications(cls, product_id):
product = cls._file.by_id(product_id)
cls.products[product_id] = []
if not product.HasAssociations:
return
for association in product.HasAssociations:
if association.is_a("IfcRelAssociatesClassification"):
cls.products[product_id].append(association.RelatingClassification.id())
@classmethod
def load_classifications(cls):
cls.classifications = {}
for classification in cls._file.by_type("IfcClassification"):
data = classification.get_info()
if cls._file.schema == "IFC2X3" and data["EditionDate"]:
data["EditionDate"] = datetime(
classification.EditionDate.YearComponent,
classification.EditionDate.MonthComponent,
classification.EditionDate.DayComponent,
).isoformat()
cls.classifications[classification.id()] = data
@classmethod
def load_references(cls):
cls.references = {}
for reference in cls._file.by_type("IfcClassificationReference"):
data = reference.get_info()
if reference.ReferencedSource:
#data["ReferencedSource"] = cls.get_referenced_source(reference.ReferencedSource)
data["ReferencedSource"] = reference.ReferencedSource.id()
cls.references[reference.id()] = data
@classmethod
def get_referenced_source(cls, reference):
if reference.is_a("IfcClassification"):
return reference
elif reference.is_a("IfcClassificationReference") and reference.ReferencedSource:
return cls.get_referenced_source(reference.ReferencedSource)
@classmethod
def load_library(cls, filepath):
cls.library_file = ifcopenshell.open(filepath)
cls.library_classifications = {}
for classification in cls.library_file.by_type("IfcClassification"):
cls.library_classifications[classification.id()] = classification.Name
@@ -0,0 +1,13 @@
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"classification": 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["classification"], name, value)
@@ -0,0 +1,13 @@
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {
"reference": 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["reference"], name, value)
@@ -0,0 +1,229 @@
import bpy
import json
import blenderbim.bim.module.classification.add_classification as add_classification
import blenderbim.bim.module.classification.remove_classification as remove_classification
import blenderbim.bim.module.classification.edit_classification as edit_classification
import blenderbim.bim.module.classification.add_reference as add_reference
import blenderbim.bim.module.classification.remove_reference as remove_reference
import blenderbim.bim.module.classification.edit_reference as edit_reference
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.classification.data import Data
from blenderbim.bim.module.classification.prop import getClassifications, getReferences
class LoadClassificationLibrary(bpy.types.Operator):
bl_idname = "bim.load_classification_library"
bl_label = "Load Classification Library"
filename_ext = ".ifc"
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
Data.load_library(self.filepath)
getClassifications(self, context)
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class AddClassification(bpy.types.Operator):
bl_idname = "bim.add_classification"
bl_label = "Add Classification"
def execute(self, context):
props = context.scene.BIMClassificationProperties
add_classification.Usecase(
IfcStore.get_file(), {"classification": Data.library_file.by_id(int(props.available_classifications))}
).execute()
Data.load()
return {"FINISHED"}
class EnableEditingClassification(bpy.types.Operator):
bl_idname = "bim.enable_editing_classification"
bl_label = "Enable Editing Classification"
classification: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.BIMClassificationProperties
while len(props.classification_attributes) > 0:
props.classification_attributes.remove(0)
classification_data = Data.classifications[self.classification]
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassification").all_attributes():
new = props.classification_attributes.add()
new.name = attribute.name()
new.is_null = classification_data[attribute.name()] is None
new.is_optional = attribute.optional()
if attribute.name() == "ReferenceTokens":
new.string_value = "" if new.is_null else json.dumps(classification_data[attribute.name()])
else:
new.string_value = "" if new.is_null else classification_data[attribute.name()]
props.active_classification_id = self.classification
return {"FINISHED"}
class DisableEditingClassification(bpy.types.Operator):
bl_idname = "bim.disable_editing_classification"
bl_label = "Disable Editing Classification"
def execute(self, context):
context.scene.BIMClassificationProperties.active_classification_id = 0
return {"FINISHED"}
class RemoveClassification(bpy.types.Operator):
bl_idname = "bim.remove_classification"
bl_label = "Remove Classification"
classification: bpy.props.IntProperty()
def execute(self, context):
self.file = IfcStore.get_file()
remove_classification.Usecase(self.file, {"classification": self.file.by_id(self.classification)}).execute()
Data.load()
return {"FINISHED"}
class EditClassification(bpy.types.Operator):
bl_idname = "bim.edit_classification"
bl_label = "Edit Classification"
def execute(self, context):
props = context.scene.BIMClassificationProperties
attributes = {}
for attribute in props.classification_attributes:
if attribute.is_null:
attributes[attribute.name] = None
elif attribute.name == "ReferenceTokens":
attributes[attribute.name] = json.loads(attribute.string_value)
else:
attributes[attribute.name] = attribute.string_value
self.file = IfcStore.get_file()
edit_classification.Usecase(
self.file, {"classification": self.file.by_id(props.active_classification_id), "attributes": attributes}
).execute()
Data.load()
bpy.ops.bim.disable_editing_classification()
return {"FINISHED"}
class EnableEditingClassificationReference(bpy.types.Operator):
bl_idname = "bim.enable_editing_classification_reference"
bl_label = "Enable Editing Classification Reference"
reference: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
props = obj.BIMClassificationReferenceProperties
while len(props.reference_attributes) > 0:
props.reference_attributes.remove(0)
reference_data = Data.references[self.reference]
for attribute in IfcStore.get_schema().declaration_by_name("IfcClassificationReference").all_attributes():
if attribute.name() == "ReferencedSource":
continue
new = props.reference_attributes.add()
new.name = attribute.name()
new.is_null = reference_data[attribute.name()] is None
new.is_optional = attribute.optional()
new.string_value = "" if new.is_null else reference_data[attribute.name()]
props.active_reference_id = self.reference
return {"FINISHED"}
class DisableEditingClassificationReference(bpy.types.Operator):
bl_idname = "bim.disable_editing_classification_reference"
bl_label = "Disable Editing Classification Reference"
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
obj.BIMClassificationReferenceProperties.active_reference_id = 0
return {"FINISHED"}
class RemoveClassificationReference(bpy.types.Operator):
bl_idname = "bim.remove_classification_reference"
bl_label = "Remove Classification Reference"
reference: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
self.file = IfcStore.get_file()
remove_reference.Usecase(
self.file,
{
"reference": self.file.by_id(self.reference),
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
},
).execute()
Data.load(obj.BIMObjectProperties.ifc_definition_id)
Data.load()
return {"FINISHED"}
class EditClassificationReference(bpy.types.Operator):
bl_idname = "bim.edit_classification_reference"
bl_label = "Edit Classification Reference"
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
props = obj.BIMClassificationReferenceProperties
attributes = {}
for attribute in props.reference_attributes:
if attribute.is_null:
attributes[attribute.name] = None
else:
attributes[attribute.name] = attribute.string_value
self.file = IfcStore.get_file()
edit_reference.Usecase(
self.file, {"reference": self.file.by_id(props.active_reference_id), "attributes": attributes}
).execute()
Data.load()
bpy.ops.bim.disable_editing_classification_reference()
return {"FINISHED"}
class AddClassificationReference(bpy.types.Operator):
bl_idname = "bim.add_classification_reference"
bl_label = "Add Classification Reference"
reference: bpy.props.IntProperty()
obj: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
self.file = IfcStore.get_file()
classification = None
props = context.scene.BIMClassificationProperties
classification_name = Data.library_classifications[int(props.available_classifications)]
for classification_id, classification in Data.classifications.items():
if classification["Name"] == classification_name:
classification = self.file.by_id(classification_id)
break
add_reference.Usecase(
self.file,
{
"reference": Data.library_file.by_id(self.reference),
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
"classification": classification
},
).execute()
Data.load(obj.BIMObjectProperties.ifc_definition_id)
Data.load()
return {"FINISHED"}
class ChangeClassificationLevel(bpy.types.Operator):
bl_idname = "bim.change_classification_level"
bl_label = "Change Classification Level"
parent_id: bpy.props.IntProperty()
def execute(self, context):
getReferences(self, context, parent_id=self.parent_id)
return {"FINISHED"}
@@ -0,0 +1,71 @@
import bpy
from blenderbim.bim.prop import StrProperty, Attribute
from blenderbim.bim.module.classification.data import Data
from bpy.types import PropertyGroup
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
classification_enum = []
def getClassifications(self, context):
global classification_enum
if len(classification_enum) < 1:
classification_enum.clear()
classification_enum.extend([(str(i), n, "") for i, n in Data.library_classifications.items()])
if classification_enum:
getReferences(self, context, parent_id=int(classification_enum[0][0]))
return classification_enum
def updateClassification(self, context):
getReferences(self, context, parent_id=int(self.available_classifications))
def getReferences(self, context, parent_id=None):
props = context.scene.BIMClassificationProperties
while len(props.available_library_references) > 0:
props.available_library_references.remove(0)
for reference in Data.library_file.by_id(parent_id).HasReferences:
new = props.available_library_references.add()
new.identification = reference.Identification or ""
new.name = reference.Name or ""
new.ifc_definition_id = reference.id()
new.has_references = bool(reference.HasReferences)
new.referenced_source
if reference.ReferencedSource.is_a("IfcClassificationReference"):
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
else:
props.active_library_referenced_source = 0
class ClassificationReference(PropertyGroup):
name: StringProperty(name="Name")
identification: StringProperty(name="Identification")
ifc_definition_id: IntProperty(name="IFC Definition ID")
has_references: BoolProperty(name="Has References")
referenced_source: IntProperty(name="IFC Definition ID")
class BIMClassificationProperties(PropertyGroup):
available_classifications: EnumProperty(
items=getClassifications, name="Available Classifications", update=updateClassification
)
classification_attributes: CollectionProperty(name="Classification Attributes", type=Attribute)
active_classification_id: IntProperty(name="Active Classification Id")
available_library_references: CollectionProperty(name="Available Library References", type=ClassificationReference)
active_library_referenced_source: IntProperty(name="Active Library Referenced Source")
active_library_reference_index: IntProperty(name="Active Library Reference Index")
class BIMClassificationReferenceProperties(PropertyGroup):
reference_attributes: CollectionProperty(name="Reference Attributes", type=Attribute)
active_reference_id: IntProperty(name="Active Reference Id")
@@ -0,0 +1,27 @@
import ifcopenshell.util.schema
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {"classification": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
references = self.get_references(self.settings["classification"])
for reference in references:
self.file.remove(reference)
self.file.remove(self.settings["classification"])
for rel in self.file.by_type("IfcRelAssociatesClassification"):
if not rel.RelatingClassification:
self.file.remove(rel)
def get_references(self, classification):
results = []
if not classification.HasReferences:
return results
for reference in classification.HasReferences:
results.append(reference)
results.extend(self.get_references(reference))
return results
@@ -0,0 +1,28 @@
import ifcopenshell.util.schema
class Usecase:
def __init__(self, file, settings=None):
self.file = file
self.settings = {"reference": None, "product": None}
for key, value in settings.items():
self.settings[key] = value
def execute(self):
total_related_objects = 0
for association in self.file.by_type("IfcRelAssociatesClassification"):
if association.RelatingClassification == self.settings["reference"] and association.RelatedObjects:
total_related_objects += len(association.RelatedObjects)
related_objects = list(association.RelatedObjects)
try:
related_objects.remove(self.settings["product"])
except:
continue
if len(related_objects):
association.RelatedObjects = related_objects
else:
self.file.remove(association)
# TODO: we only handle lightweight classifications here
if total_related_objects == 1:
self.file.remove(self.settings["reference"])
@@ -0,0 +1,159 @@
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.classification.data import Data
class BIM_PT_classifications(Panel):
bl_label = "IFC Classifications"
bl_idname = "BIM_PT_classifications"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
if not Data.is_loaded:
Data.load()
self.props = context.scene.BIMClassificationProperties
if Data.library_file:
row = self.layout.row(align=True)
row.prop(self.props, "available_classifications", text="")
row.operator("bim.load_classification_library", text="", icon="IMPORT")
row.operator("bim.add_classification", text="", icon="ADD")
else:
row = self.layout.row(align=True)
row.label(text="No Active Classification Library")
row.operator("bim.load_classification_library", text="", icon="IMPORT")
for classification_id, classification in Data.classifications.items():
if self.props.active_classification_id == classification_id:
self.draw_editable_ui(classification)
else:
self.draw_ui(classification_id, classification)
def draw_editable_ui(self, classification):
row = self.layout.row(align=True)
row.prop(self.props.classification_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
row.operator("bim.edit_classification", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_classification", text="", icon="X")
for attribute in self.props.classification_attributes:
if attribute.name == "Name":
continue
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
def draw_ui(self, classification_id, classification):
row = self.layout.row(align=True)
row.label(text=classification["Name"], icon="ASSET_MANAGER")
if not self.props.active_classification_id:
op = row.operator("bim.enable_editing_classification", text="", icon="GREASEPENCIL")
op.classification = classification_id
row.operator("bim.remove_classification", text="", icon="X").classification = classification_id
class BIM_PT_classification_references(Panel):
bl_label = "IFC Classification References"
bl_idname = "BIM_PT_classification_references"
bl_options = {"DEFAULT_CLOSED"}
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):
obj = context.active_object
self.oprops = obj.BIMObjectProperties
self.sprops = context.scene.BIMClassificationProperties
self.props = obj.BIMClassificationReferenceProperties
self.file = IfcStore.get_file()
if not Data.is_loaded:
Data.load()
if self.oprops.ifc_definition_id not in Data.products:
Data.load(self.oprops.ifc_definition_id)
self.draw_add_ui()
reference_ids = Data.products[self.oprops.ifc_definition_id]
if not reference_ids:
row = self.layout.row(align=True)
row.label(text="No References")
for reference_id in reference_ids:
reference = Data.references[reference_id]
if self.props.active_reference_id == reference_id:
self.draw_editable_ui(reference)
else:
self.draw_ui(reference_id, reference)
def draw_add_ui(self):
if not self.sprops.available_classifications:
return
name = Data.library_classifications[int(self.sprops.available_classifications)]
if name in [c["Name"] for c in Data.classifications.values()]:
row = self.layout.row(align=True)
row.prop(self.sprops, "available_classifications", text="")
if self.sprops.active_library_referenced_source:
op = row.operator("bim.change_classification_level", text="", icon="FRAME_PREV")
op.parent_id = self.sprops.active_library_referenced_source
op = row.operator("bim.add_classification_reference", text="", icon="ADD")
op.reference = self.sprops.available_library_references[
self.sprops.active_library_reference_index
].ifc_definition_id
self.layout.template_list(
"BIM_UL_classifications",
"",
self.sprops,
"available_library_references",
self.sprops,
"active_library_reference_index",
)
def draw_editable_ui(self, reference):
row = self.layout.row(align=True)
row.prop(self.props.reference_attributes.get("Name"), "string_value", text="", icon="ASSET_MANAGER")
row.operator("bim.edit_classification_reference", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_classification_reference", text="", icon="X")
for attribute in self.props.reference_attributes:
if attribute.name == "Name":
continue
row = self.layout.row(align=True)
row.prop(attribute, "string_value", text=attribute.name)
if attribute.is_optional:
row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
def draw_ui(self, reference_id, reference):
row = self.layout.row(align=True)
if self.file.schema == "IFC2X3":
name = reference["ItemReference"] or "No Identification"
else:
name = reference["Identification"] or "No Identification"
row.label(text=name, icon="ASSET_MANAGER")
row.label(text=reference["Name"] or "")
if not self.props.active_reference_id:
op = row.operator("bim.enable_editing_classification_reference", text="", icon="GREASEPENCIL")
op.reference = reference_id
row.operator("bim.remove_classification_reference", text="", icon="X").reference = reference_id
class BIM_UL_classifications(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
if item.has_references:
op = layout.operator("bim.change_classification_level", text="", icon="DISCLOSURE_TRI_RIGHT")
op.parent_id = item.ifc_definition_id
layout.label(text=item.identification)
layout.label(text=item.name)