Files
IfcOpenShell/src/bonsai/scripts/generate_demo_library.py
T
2026-01-26 17:13:18 +05:00

430 lines
19 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from pathlib import Path
import bpy
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.material
import ifcopenshell.api.owner
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.api.style
import ifcopenshell.api.unit
import ifcopenshell.util.schema
import ifcopenshell.util.shape_builder
import bonsai.tool as tool
def sync_guids(target_file: ifcopenshell.file, source_file: ifcopenshell.file) -> None:
def sync_guid(element: ifcopenshell.entity_instance) -> None:
"""Try to sync `element` guid with `source_file`.
Sync is assigning guid based on the first element from `source_file` that has a matching type and name.
"""
# In IFC2X3 IfcProject is not IfcContext.
if element.is_a("IfcTypeProduct") or element.is_a("IfcContext") or element.is_a("IfcProject"):
assert element.Name, element
elements = source_file.by_type(element.is_a(), False)
for element_ in elements:
if element.Name == element_.Name:
element.GlobalId = element_.GlobalId
return
print(
f"WARNING! Couldn't find a matching element in the GUID source to sync guid for '{element.Name}' ({element})."
)
def sync_pset(pset: ifcopenshell.entity_instance) -> None:
element_types = pset.DefinesType
assert len(element_types) == 1
element_type: ifcopenshell.entity_instance = element_types[0]
try:
element_type_ = source_file.by_guid(element_type.GlobalId)
except RuntimeError:
# New type with pset was added.
print(f"WARNING! Couldn't find a matching pset for '{pset.Name}' ({pset}).")
return
assert len(element_type_.HasPropertySets) == 1
pset_ = element_type_.HasPropertySets[0]
assert pset.Name == pset_.Name
pset.GlobalId = pset_.GlobalId
def sync_rel(rel: ifcopenshell.entity_instance) -> None:
if rel.is_a("IfcRelAssociatesMaterial"):
related_objects = rel.RelatedObjects
assert len(related_objects) == 1
related_object = related_objects[0]
try:
related_object_ = source_file.by_guid(related_object.GlobalId)
except RuntimeError:
# New type with material was added.
print(f"WARNING! Couldn't find a matching rel for '{rel}'.")
return
rel_ = next(r for r in related_object_.HasAssociations if r.is_a("IfcRelAssociatesMaterial"))
assert rel_
rel.GlobalId = rel_.GlobalId
elif rel.is_a("IfcRelDeclares"):
context = rel.RelatingContext
try:
context_ = source_file.by_guid(context.GlobalId)
except RuntimeError:
# New context was added.
print(f"WARNING! Couldn't find a matching context for '{context}'.")
return
rel_ = context_.Declares[0]
rel.GlobalId = rel_.GlobalId
else:
print(f"WARNING! Couldn't find a matching element in the GUID source to sync guid for '{rel}'.")
roots = target_file.by_type("IfcRoot")
psets: list[ifcopenshell.entity_instance] = []
rels: list[ifcopenshell.entity_instance] = []
for element in roots:
if element.is_a("IfcPropertySet"):
psets.append(element)
continue
elif element.is_a("IfcRelationship"):
rels.append(element)
continue
sync_guid(element)
for rel in rels:
sync_rel(rel)
for pset in psets:
sync_pset(pset)
class LibraryGenerator:
output_filename = "Demo Library.ifc"
guid_source: ifcopenshell.file
"""Guid sync is needed to avoid unnecessary diffs
and to avoid `append_asset` from identifying existing elements and as new ones.
"""
def generate(self, schema: ifcopenshell.util.schema.IFC_SCHEMA) -> None:
assert bpy.context.blend_data
opened_blend_file = Path(bpy.context.blend_data.filepath)
assert (
opened_blend_file.name == "demo-library.blend"
), "This script must be run from the demo-library.blend file as it's using Blender objects to create representations."
libraries_path = opened_blend_file.parent.parent / "bonsai" / "bim" / "data" / "libraries"
guid_source_filepath = libraries_path / f"{schema} {self.output_filename}"
self.guid_source = ifcopenshell.open(guid_source_filepath)
ifcopenshell.api.pre_listeners = {}
ifcopenshell.api.post_listeners = {}
self.file = ifcopenshell.api.project.create_file(schema)
self.builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file)
if schema == "IFC2X3":
ifcopenshell.api.owner.settings.factory_reset()
person = ifcopenshell.api.owner.add_person(self.file)
organization = ifcopenshell.api.owner.add_organisation(self.file)
user = ifcopenshell.api.owner.add_person_and_organisation(
self.file, person=person, organisation=organization
)
application = ifcopenshell.api.owner.add_application(
self.file, application_full_name="Bonsai", application_identifier="Bonsai"
)
# Override Bonsai methods for duration of the script.
ifcopenshell.api.owner.settings.get_user = lambda x: user
ifcopenshell.api.owner.settings.get_application = lambda x: application
# Basic project setup.
self.project = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject", name="Bonsai Demo")
if schema != "IFC2X3":
self.library = ifcopenshell.api.root.create_entity(
self.file, ifc_class="IfcProjectLibrary", name="Bonsai Demo Library"
)
ifcopenshell.api.project.assign_declaration(
self.file, definitions=[self.library], relating_context=self.project
)
ifcopenshell.api.unit.assign_unit(self.file, length={"is_metric": True, "raw": "METERS"})
model = ifcopenshell.api.context.add_context(self.file, context_type="Model")
plan = ifcopenshell.api.context.add_context(self.file, context_type="Plan")
self.representations = {
"model_body": ifcopenshell.api.context.add_context(
self.file,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model,
),
"plan_body": ifcopenshell.api.context.add_context(
self.file,
context_type="Plan",
context_identifier="Body",
target_view="PLAN_VIEW",
parent=plan,
),
"model_annotation": ifcopenshell.api.context.add_context(
self.file,
context_type="Model",
context_identifier="Annotation",
target_view="MODEL_VIEW",
parent=plan,
),
}
self.material = ifcopenshell.api.material.add_material(self.file, name="Unknown")
# IfcWallType.
self.create_layer_type("IfcWallType", "WAL50", 0.05)
self.create_layer_type("IfcWallType", "WAL100", 0.1)
self.create_layer_type("IfcWallType", "WAL200", 0.2)
self.create_layer_type("IfcWallType", "WAL300", 0.3)
# IfcCoveringType.
self.create_layer_type("IfcCoveringType", "COV10", 0.01)
product = self.create_layer_type("IfcCoveringType", "COV20", 0.02)
pset = ifcopenshell.api.pset.add_pset(self.file, product=product, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"LayerSetDirection": "AXIS2"})
product = self.create_layer_type("IfcCoveringType", "COV30", 0.03)
pset = ifcopenshell.api.pset.add_pset(self.file, product=product, name="EPset_Parametric")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"LayerSetDirection": "AXIS3"})
if schema != "IFC2X3":
self.create_layer_type("IfcRampType", "RAM200", 0.2)
profile = self.file.create_entity("IfcCircleProfileDef", ProfileType="AREA", Radius=0.3)
self.create_profile_type("IfcPileType", "P1", profile)
self.create_layer_type("IfcSlabType", "FLR200", 0.2)
self.create_layer_type("IfcSlabType", "FLR300", 0.3)
if schema != "IFC2X3":
# No profile sets in IFC2X3 :(
profile = self.file.create_entity(
"IfcRectangleProfileDef", ProfileName="500x600", ProfileType="AREA", XDim=0.5, YDim=0.6
)
self.create_profile_type("IfcColumnType", "C1", profile)
profile = self.file.create_entity(
"IfcCircleHollowProfileDef",
ProfileName="500.0x5.0 CHS",
ProfileType="AREA",
Radius=0.25,
WallThickness=0.005,
)
self.create_profile_type("IfcColumnType", "C2", profile)
profile = self.file.create_entity(
"IfcRectangleHollowProfileDef",
ProfileName="150x75x2.0 RHS",
ProfileType="AREA",
XDim=0.075,
YDim=0.15,
WallThickness=0.002,
InnerFilletRadius=0.005,
OuterFilletRadius=0.005,
)
self.create_profile_type("IfcColumnType", "C3", profile)
profile = self.file.create_entity(
"IfcIShapeProfileDef",
ProfileName="DEMO-I",
ProfileType="AREA",
OverallWidth=0.1,
OverallDepth=0.2,
WebThickness=0.005,
FlangeThickness=0.01,
FilletRadius=0.005,
)
self.create_profile_type("IfcBeamType", "B1", profile)
profile = self.file.create_entity(
"IfcCShapeProfileDef",
ProfileName="DEMO-C",
ProfileType="AREA",
Depth=0.2,
Width=0.1,
WallThickness=0.0015,
Girth=0.03,
InternalFilletRadius=0.005,
)
self.create_profile_type("IfcBeamType", "B2", profile)
self.create_type(
"IfcWindowType" if schema != "IFC2X3" else "IfcWindowStyle",
"WT01",
{"model_body": "Window", "plan_body": "Window-Plan"},
)
self.create_type(
"IfcDoorType" if schema != "IFC2X3" else "IfcDoorStyle",
"DT01",
{"model_body": "Door", "plan_body": "Door-Plan"},
)
self.create_type("IfcFurnitureType", "BUN01", {"model_body": "Bunny", "plan_body": "Bunny-Plan"})
# IfcAnnotation types.
self.create_symbol_type("SETOUT-POINT", "setout-point")
self.create_symbol_type("CONTROL-POINT", "control-point")
self.create_symbol_type("TRAVERSE-POINT", "traverse-point")
self.create_line_type("DASHED", "dashed")
self.create_line_type("FINE", "fine")
self.create_line_type("THIN", "thin")
self.create_line_type("MEDIUM", "medium")
self.create_line_type("THICK", "thick")
self.create_line_type("STRONG", "strong")
self.create_text_type(
"SETOUT-TAG", "setout-tag", ["E ``round({{easting}}, 0.001)``", "N ``round({{northing}}, 0.001)``"]
)
self.create_text_type("DOOR-TAG", "door-tag", ["{{type.Name}}", "{{Name}}"])
self.create_text_type("WINDOW-TAG", "window-tag", ["{{Name}}"])
self.create_text_type(
"SPACE-TAG",
"space-tag",
["{{Name}}", "{{Description}}", "``round({{Qto_SpaceBaseQuantities.NetFloorArea}}, 0.01)``"],
)
self.create_text_type("MATERIAL-TAG", "rectangle-tag", ["{{material.Name}}"])
self.create_text_type("TYPE-TAG", "capsule-tag", ["{{type.Name}}"])
self.create_text_type("NAME-TAG", "capsule-tag", ["{{Name}}"])
sync_guids(self.file, self.guid_source)
self.file.write(libraries_path / f"{self.file.schema} {self.output_filename}")
if schema == "IFC2X3":
ifcopenshell.api.owner.settings.restore()
def create_symbol_type(self, name: str, symbol: str) -> None:
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTypeProduct", name=name)
element.ApplicableOccurrence = "IfcAnnotation/SYMBOL"
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Symbol": symbol})
def create_line_type(self, name: str, classes: str) -> None:
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTypeProduct", name=name)
element.ApplicableOccurrence = "IfcAnnotation/LINEWORK"
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Classes": classes})
def create_text_type(self, name: str, symbol: str, literals: list[str]) -> None:
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTypeProduct", name=name)
element.ApplicableOccurrence = "IfcAnnotation/TEXT"
pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="EPset_Annotation")
ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Symbol": symbol})
items: list[ifcopenshell.entity_instance] = []
for literal in literals:
origin = self.builder.create_axis2_placement_3d()
items.append(
self.file.create_entity(
"IfcTextLiteralWithExtent",
literal,
origin,
"RIGHT",
self.file.create_entity("IfcPlanarExtent", 1000, 1000),
"center",
),
)
representation = self.file.create_entity(
"IfcShapeRepresentation",
self.representations["model_annotation"],
self.representations["model_annotation"].ContextIdentifier,
"Annotation2D",
items,
)
ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=representation)
def create_layer_type(self, ifc_class: str, name: str, thickness: float) -> ifcopenshell.entity_instance:
element = ifcopenshell.api.root.create_entity(self.file, ifc_class=ifc_class, name=name)
rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialLayerSet")
assert isinstance(rel, ifcopenshell.entity_instance)
layer_set = rel.RelatingMaterial
layer = ifcopenshell.api.material.add_layer(self.file, layer_set=layer_set, material=self.material)
layer.LayerThickness = thickness
if self.file.schema != "IFC2X3":
ifcopenshell.api.project.assign_declaration(self.file, definitions=[element], relating_context=self.library)
return element
def create_profile_type(self, ifc_class: str, name: str, profile: ifcopenshell.entity_instance) -> None:
element = ifcopenshell.api.root.create_entity(self.file, ifc_class=ifc_class, name=name)
rel = ifcopenshell.api.material.assign_material(self.file, products=[element], type="IfcMaterialProfileSet")
assert isinstance(rel, ifcopenshell.entity_instance)
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.material.add_profile(
self.file, profile_set=profile_set, material=self.material
)
ifcopenshell.api.material.assign_profile(self.file, material_profile=material_profile, profile=profile)
if self.file.schema != "IFC2X3":
ifcopenshell.api.project.assign_declaration(self.file, definitions=[element], relating_context=self.library)
def create_type(self, ifc_class: str, name: str, representations: dict[str, str]) -> None:
""" "
:param representations: Mapping of representation contexts to Blender objects names to be used
as a representation source.
"""
element = ifcopenshell.api.root.create_entity(self.file, ifc_class=ifc_class, name=name)
for rep_name, obj_name in representations.items():
obj = bpy.data.objects[obj_name]
assert isinstance(obj.data, bpy.types.Mesh)
representation = ifcopenshell.api.geometry.add_representation(
self.file,
context=self.representations[rep_name],
blender_object=obj,
geometry=obj.data,
total_items=max(1, len(obj.material_slots)),
)
assert representation
styles: list[ifcopenshell.entity_instance] = []
for slot in obj.material_slots:
material = slot.material
assert material
style = ifcopenshell.api.style.add_style(self.file, name=material.name)
attributes = tool.Style.get_surface_shading_attributes(material)
if self.file.schema == "IFC2X3":
del attributes["Transparency"]
ifcopenshell.api.style.add_surface_style(
self.file,
style=style,
ifc_class="IfcSurfaceStyleShading",
attributes=attributes,
)
styles.append(style)
if styles:
ifcopenshell.api.style.assign_representation_styles(
self.file, shape_representation=representation, styles=styles
)
ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=representation)
if self.file.schema != "IFC2X3":
ifcopenshell.api.project.assign_declaration(self.file, definitions=[element], relating_context=self.library)
if __name__ == "__main__":
LibraryGenerator().generate("IFC2X3")
LibraryGenerator().generate("IFC4")
LibraryGenerator().generate("IFC4X3")