mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Refactor pset module to follow the same design patterns as other modules, in preparation for complex property / quantity handling in the future
This commit is contained in:
@@ -2,6 +2,8 @@ import bpy
|
||||
import json
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.unit
|
||||
import ifcopenshell.util.pset
|
||||
import ifcopenshell.util.attribute
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.pset.data import Data
|
||||
from blenderbim.bim.module.pset.qto_calculator import QtoCalculator
|
||||
@@ -31,33 +33,100 @@ class EnablePsetEditing(bpy.types.Operator):
|
||||
obj = bpy.data.objects.get(self.obj)
|
||||
elif self.obj_type == "Material":
|
||||
obj = bpy.data.materials.get(self.obj)
|
||||
props = obj.PsetProperties
|
||||
self.props = obj.PsetProperties
|
||||
|
||||
while len(props.properties) > 0:
|
||||
props.properties.remove(0)
|
||||
while len(self.props.properties) > 0:
|
||||
self.props.properties.remove(0)
|
||||
|
||||
data = Data.psets if self.pset_id in Data.psets else Data.qtos
|
||||
props.active_pset_name = data[self.pset_id]["Name"]
|
||||
for prop in data[self.pset_id]["Properties"]:
|
||||
new = props.properties.add()
|
||||
new.name = prop["Name"]
|
||||
new.is_null = prop["is_null"]
|
||||
if prop["type"] == "string":
|
||||
new.string_value = prop["value"] or ""
|
||||
elif prop["type"] == "integer":
|
||||
new.int_value = prop["value"] or 0
|
||||
elif prop["type"] == "float":
|
||||
new.float_value = prop["value"] or 0.0
|
||||
elif prop["type"] == "boolean":
|
||||
new.bool_value = prop["value"] or False
|
||||
elif prop["type"] == "enum":
|
||||
new.enum_items = json.dumps(prop["enum_items"])
|
||||
if prop["value"]:
|
||||
new.enum_value = prop["value"]
|
||||
pset_data = data[self.pset_id]
|
||||
self.props.active_pset_name = pset_data["Name"]
|
||||
|
||||
props.active_pset_id = self.pset_id
|
||||
templates = ifcopenshell.util.pset.get_template("IFC4")
|
||||
pset_template = templates.get_by_name(pset_data["Name"])
|
||||
|
||||
if pset_template:
|
||||
self.load_from_pset_template(pset_template, pset_data)
|
||||
else:
|
||||
self.load_from_pset_data(pset_data)
|
||||
|
||||
self.props.active_pset_id = self.pset_id
|
||||
return {"FINISHED"}
|
||||
|
||||
def load_from_pset_template(self, pset_template, pset_data):
|
||||
data = {Data.properties[p]["Name"]: Data.properties[p]["NominalValue"] for p in pset_data["Properties"]}
|
||||
for prop_template in pset_template.HasPropertyTemplates:
|
||||
if not prop_template.is_a("IfcSimplePropertyTemplate"):
|
||||
continue # Other types not yet supported
|
||||
|
||||
if prop_template.TemplateType == "P_SINGLEVALUE":
|
||||
try:
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(
|
||||
IfcStore.get_schema().declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
|
||||
)
|
||||
except:
|
||||
# TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix
|
||||
# this we need to generate the IFC2X3 pset template definitions.
|
||||
continue
|
||||
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
|
||||
data_type = "enum"
|
||||
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
|
||||
elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
|
||||
data_type = "float"
|
||||
elif prop_template.TemplateType == "Q_COUNT":
|
||||
data_type = "integer"
|
||||
else:
|
||||
continue # Other types not yet supported
|
||||
|
||||
new = self.props.properties.add()
|
||||
new.name = prop_template.Name
|
||||
new.is_null = data.get(prop_template.Name, None) is None
|
||||
new.data_type = data_type
|
||||
|
||||
if data_type == "string":
|
||||
new.string_value = "" if new.is_null else data[prop_template.Name]
|
||||
elif data_type == "integer":
|
||||
new.int_value = 0 if new.is_null else data[prop_template.Name]
|
||||
elif data_type == "float":
|
||||
new.float_value = 0.0 if new.is_null else data[prop_template.Name]
|
||||
elif data_type == "boolean":
|
||||
new.bool_value = False if new.is_null else data[prop_template.Name]
|
||||
elif data_type == "enum":
|
||||
new.enum_items = json.dumps(enum_items)
|
||||
if data.get(prop_template.Name):
|
||||
new.enum_value = data[prop_template.Name]
|
||||
|
||||
def load_from_pset_data(self, pset_data):
|
||||
for prop_id in pset_data["Properties"]:
|
||||
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.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
|
||||
|
||||
|
||||
class DisablePsetEditing(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_pset_editing"
|
||||
@@ -98,22 +167,19 @@ class EditPset(bpy.types.Operator):
|
||||
properties = json.loads(self.properties)
|
||||
else:
|
||||
data = Data.psets if pset_id in Data.psets else Data.qtos
|
||||
for prop in data[pset_id]["Properties"]:
|
||||
blender_prop = props.properties.get(prop["Name"])
|
||||
if not blender_prop:
|
||||
continue
|
||||
if blender_prop.is_null:
|
||||
properties[prop["Name"]] = None
|
||||
elif prop["type"] == "string":
|
||||
properties[prop["Name"]] = blender_prop.string_value
|
||||
elif prop["type"] == "boolean":
|
||||
properties[prop["Name"]] = blender_prop.bool_value
|
||||
elif prop["type"] == "integer":
|
||||
properties[prop["Name"]] = blender_prop.int_value
|
||||
elif prop["type"] == "float":
|
||||
properties[prop["Name"]] = blender_prop.float_value
|
||||
elif prop["type"] == "enum":
|
||||
properties[prop["Name"]] = blender_prop.enum_value
|
||||
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
|
||||
|
||||
if pset_id in Data.psets:
|
||||
ifcopenshell.api.run(
|
||||
|
||||
@@ -6,6 +6,8 @@ from blenderbim.bim.ifc import IfcStore
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if "is_expanded" not in pset:
|
||||
pset["is_expanded"] = True
|
||||
icon = "TRIA_DOWN" if pset["is_expanded"] else "TRIA_RIGHT"
|
||||
row.operator("bim.toggle_pset_expansion", icon=icon, text="", emboss=False).pset_id = pset_id
|
||||
if not props.active_pset_id:
|
||||
@@ -34,20 +36,21 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type):
|
||||
op.obj_type = obj_type
|
||||
if pset["is_expanded"]:
|
||||
if props.active_pset_id == pset_id:
|
||||
for prop in pset["Properties"]:
|
||||
for prop in props.properties:
|
||||
draw_psetqto_editable_ui(box, props, prop)
|
||||
else:
|
||||
has_props_displayed = False
|
||||
for prop in pset["Properties"]:
|
||||
for prop_id in pset["Properties"]:
|
||||
prop = Data.properties[prop_id]
|
||||
if context.preferences.addons["blenderbim"].preferences.should_hide_empty_props and (
|
||||
prop["value"] is None or prop["value"] == ""
|
||||
prop["NominalValue"] is None or prop["NominalValue"] == ""
|
||||
):
|
||||
continue
|
||||
has_props_displayed = True
|
||||
row = box.row(align=True)
|
||||
row.scale_y = 0.8
|
||||
row.label(text=prop["Name"])
|
||||
row.label(text=str(prop["value"]))
|
||||
row.label(text=str(prop["NominalValue"]))
|
||||
if not has_props_displayed:
|
||||
row = box.row()
|
||||
row.scale_y = 0.8
|
||||
@@ -56,33 +59,32 @@ 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)
|
||||
blender_prop = props.properties.get(prop["Name"])
|
||||
if prop["type"] == "string":
|
||||
row.prop(blender_prop, "string_value", text=prop["Name"])
|
||||
elif prop["type"] == "integer":
|
||||
row.prop(blender_prop, "int_value", text=prop["Name"])
|
||||
elif prop["type"] == "float":
|
||||
row.prop(blender_prop, "float_value", text=prop["Name"])
|
||||
elif prop["type"] == "boolean":
|
||||
row.prop(blender_prop, "bool_value", text=prop["Name"])
|
||||
elif prop["type"] == "enum":
|
||||
row.prop(blender_prop, "enum_value", text=prop["Name"])
|
||||
row.prop(blender_prop, "is_null", icon="RADIOBUT_OFF" if blender_prop.is_null else "RADIOBUT_ON", text="")
|
||||
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="")
|
||||
if (
|
||||
"length" in prop["Name"].lower()
|
||||
or "width" in prop["Name"].lower()
|
||||
or "height" in prop["Name"].lower()
|
||||
or "depth" in prop["Name"].lower()
|
||||
or "perimeter" in prop["Name"].lower()
|
||||
"length" in prop.name.lower()
|
||||
or "width" in prop.name.lower()
|
||||
or "height" in prop.name.lower()
|
||||
or "depth" in prop.name.lower()
|
||||
or "perimeter" in prop.name.lower()
|
||||
):
|
||||
op = row.operator("bim.guess_quantity", icon="IPO_EASE_IN_OUT", text="")
|
||||
op.prop = prop["Name"]
|
||||
elif "area" in prop["Name"].lower():
|
||||
op.prop = prop.name
|
||||
elif "area" in prop.name.lower():
|
||||
op = row.operator("bim.guess_quantity", icon="MESH_CIRCLE", text="")
|
||||
op.prop = prop["Name"]
|
||||
elif "volume" in prop["Name"].lower():
|
||||
op.prop = prop.name
|
||||
elif "volume" in prop.name.lower():
|
||||
op = row.operator("bim.guess_quantity", icon="SPHERE", text="")
|
||||
op.prop = prop["Name"]
|
||||
op.prop = prop.name
|
||||
|
||||
|
||||
class BIM_PT_object_psets(Panel):
|
||||
|
||||
@@ -7,18 +7,17 @@ class Data:
|
||||
products = {}
|
||||
psets = {}
|
||||
qtos = {}
|
||||
properties = {}
|
||||
|
||||
@classmethod
|
||||
def purge(cls):
|
||||
cls.products = {}
|
||||
cls.psets = {}
|
||||
cls.qtos = {}
|
||||
cls.properties = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, file, product_id):
|
||||
cls._file = file
|
||||
cls._psetqto = ifcopenshell.util.pset.get_template("IFC4")
|
||||
cls._schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema)
|
||||
if not file:
|
||||
return
|
||||
product = file.by_id(product_id)
|
||||
@@ -59,133 +58,47 @@ class Data:
|
||||
|
||||
@classmethod
|
||||
def add_pset(cls, pset, product_id):
|
||||
new_pset = {
|
||||
"Name": pset.Name,
|
||||
"is_expanded": True,
|
||||
"Properties": cls.get_properties_from_template(pset.Name) or [],
|
||||
}
|
||||
cls.products[product_id]["psets"].add(int(pset.id()))
|
||||
cls.psets[int(pset.id())] = new_pset
|
||||
try:
|
||||
if hasattr(pset, "HasProperties"):
|
||||
props = pset.HasProperties
|
||||
elif hasattr(pset, "Properties"): # For IfcMaterialProperties
|
||||
props = pset.Properties
|
||||
except:
|
||||
return # I've seen ArchiCAD produce invalid IFCs with empty data
|
||||
# Invalid IFC, but some vendors like Solidworks do this so we accomodate it
|
||||
if not props:
|
||||
return
|
||||
data = pset.get_info()
|
||||
del data["OwnerHistory"]
|
||||
del data["HasProperties"]
|
||||
if hasattr(pset, "HasProperties"):
|
||||
props = pset.HasProperties or []
|
||||
elif hasattr(pset, "Properties"):
|
||||
props = pset.Properties or []
|
||||
# TODO: support more than single values
|
||||
data["Properties"] = [p.id() for p in props if p.is_a("IfcPropertySingleValue")]
|
||||
cls.psets[pset.id()] = data
|
||||
cls.products[product_id]["psets"].add(pset.id())
|
||||
for prop in props:
|
||||
if prop.is_a("IfcPropertySingleValue") and prop.NominalValue:
|
||||
has_existing_prop = False
|
||||
for existing_prop in new_pset["Properties"]:
|
||||
if existing_prop["Name"] == prop.Name:
|
||||
if prop.NominalValue is None:
|
||||
existing_prop["value"] = None
|
||||
elif existing_prop["type"] == "string":
|
||||
existing_prop["value"] = str(prop.NominalValue.wrappedValue)
|
||||
elif existing_prop["type"] == "float":
|
||||
existing_prop["value"] = float(prop.NominalValue.wrappedValue)
|
||||
elif existing_prop["type"] == "integer":
|
||||
existing_prop["value"] = int(prop.NominalValue.wrappedValue)
|
||||
elif existing_prop["type"] == "boolean":
|
||||
existing_prop["value"] = bool(prop.NominalValue.wrappedValue)
|
||||
elif existing_prop["type"] == "enum":
|
||||
existing_prop["value"] = str(prop.NominalValue.wrappedValue)
|
||||
existing_prop["is_null"] = prop.NominalValue.wrappedValue is None
|
||||
has_existing_prop = True
|
||||
break
|
||||
if not has_existing_prop:
|
||||
value = prop.NominalValue.wrappedValue
|
||||
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_pset["Properties"].append(
|
||||
{
|
||||
"Name": prop.Name,
|
||||
"value": value,
|
||||
"type": data_type,
|
||||
"enum_items": [],
|
||||
"is_null": prop.NominalValue.wrappedValue is None,
|
||||
}
|
||||
)
|
||||
# TODO: support more than single values
|
||||
if prop.is_a("IfcPropertySingleValue"):
|
||||
cls.load_prop(prop)
|
||||
|
||||
@classmethod
|
||||
def load_prop(cls, prop):
|
||||
data = prop.get_info()
|
||||
if prop.is_a("IfcProperty"):
|
||||
# TODO: support units
|
||||
del data["Unit"]
|
||||
if prop.NominalValue is not None:
|
||||
data["NominalValue"] = prop.NominalValue.wrappedValue
|
||||
elif prop.is_a("IfcPhysicalQuantity"):
|
||||
# TODO: support units
|
||||
del data["Unit"]
|
||||
# For convenience, which trumps correctness in this case
|
||||
data["NominalValue"] = prop[3]
|
||||
cls.properties[prop.id()] = data
|
||||
|
||||
@classmethod
|
||||
def add_qto(cls, qto, product_id):
|
||||
new_qto = {
|
||||
"Name": qto.Name,
|
||||
"is_expanded": True,
|
||||
"Properties": cls.get_properties_from_template(qto.Name) or [],
|
||||
}
|
||||
cls.products[product_id]["qtos"].add(int(qto.id()))
|
||||
cls.qtos[int(qto.id())] = new_qto
|
||||
for prop in qto.Quantities or []:
|
||||
if prop.is_a("IfcPhysicalSimpleQuantity"):
|
||||
value = prop[3]
|
||||
has_existing_prop = False
|
||||
for existing_prop in new_qto["Properties"]:
|
||||
if existing_prop["Name"] == prop.Name:
|
||||
existing_prop["value"] = float(value)
|
||||
existing_prop["is_null"] = value is None
|
||||
has_existing_prop = True
|
||||
break
|
||||
if not has_existing_prop:
|
||||
new_qto["Properties"].append(
|
||||
{
|
||||
"Name": prop.Name,
|
||||
"value": float(value),
|
||||
"type": "float",
|
||||
"enum_items": [],
|
||||
"is_null": value is None,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_properties_from_template(cls, name):
|
||||
template = cls._psetqto.get_by_name(name)
|
||||
if not template:
|
||||
return
|
||||
properties = []
|
||||
for prop_template in template.HasPropertyTemplates:
|
||||
if not prop_template.is_a("IfcSimplePropertyTemplate"):
|
||||
continue # Other types not yet supported
|
||||
enum_items = []
|
||||
|
||||
if prop_template.TemplateType == "P_SINGLEVALUE":
|
||||
try:
|
||||
data_type = ifcopenshell.util.attribute.get_primitive_type(
|
||||
cls._schema.declaration_by_name(prop_template.PrimaryMeasureType or "IfcLabel")
|
||||
)
|
||||
except:
|
||||
# TODO: Occurs if the data type is something that exists in IFC4 and not in IFC2X3. To fully fix
|
||||
# this we need to generate the IFC2X3 pset template definitions.
|
||||
continue
|
||||
elif prop_template.TemplateType == "P_ENUMERATEDVALUE":
|
||||
data_type = "enum"
|
||||
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
|
||||
elif prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
|
||||
data_type = "float"
|
||||
elif prop_template.TemplateType == "Q_COUNT":
|
||||
data_type = "integer"
|
||||
else:
|
||||
continue # Other types not yet supported
|
||||
|
||||
properties.append(
|
||||
{
|
||||
"Name": prop_template.Name,
|
||||
"value": None,
|
||||
"type": data_type,
|
||||
"enum_items": enum_items,
|
||||
"is_null": True,
|
||||
}
|
||||
)
|
||||
return properties
|
||||
data = qto.get_info()
|
||||
del data["OwnerHistory"]
|
||||
del data["Quantities"]
|
||||
# TODO: support more than just simple quantities
|
||||
# We call it properties for convenience, not for correctness
|
||||
data["Properties"] = [q.id() for q in qto.Quantities or [] if q.is_a("IfcPhysicalSimpleQuantity")]
|
||||
cls.qtos[qto.id()] = data
|
||||
cls.products[product_id]["qtos"].add(qto.id())
|
||||
for quantity in qto.Quantities or []:
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
cls.load_prop(quantity)
|
||||
|
||||
Reference in New Issue
Block a user