Files
2026-03-13 08:56:29 +01:00

160 lines
5.2 KiB
Python

# =============================================================================
# writer.py
# Creates and manages the IFC file structure:
# IfcProject → IfcSite → IfcBuilding → IfcBuildingStorey (one per level)
#
# Also provides StoreyManager which lazily creates storeys on demand
# as the traversal encounters new level names.
# =============================================================================
import ifcopenshell
import ifcopenshell.api
def create_ifc_scaffold(
project_name: str = "Default Project",
site_name: str = "Default Site",
building_name: str = "Default Building",
) -> tuple:
"""
Create the IFC file with the required project/site/building hierarchy.
Returns:
(ifc_file, site, building, body_context)
- ifc_file: The ifcopenshell file object
- site: The IfcSite entity
- building: The IfcBuilding entity (storeys are assigned under this)
- body_context: The Body geometry subcontext for shape representations
"""
ifc = ifcopenshell.file(schema="IFC4X3")
# Project
project = ifcopenshell.api.run(
"root.create_entity", ifc,
ifc_class="IfcProject",
name=project_name,
)
# Units — millimetres (matching Revit/Speckle source data)
# This avoids any mm→m conversion errors and keeps coordinates at full precision
ifcopenshell.api.run(
"unit.assign_unit", ifc,
length={"is_metric": True, "raw": "MILLIMETRES"},
)
# Geometry contexts
model_ctx = ifcopenshell.api.run(
"context.add_context", ifc,
context_type="Model",
)
body_ctx = ifcopenshell.api.run(
"context.add_context", ifc,
context_type="Model",
context_identifier="Body",
target_view="MODEL_VIEW",
parent=model_ctx,
)
# Spatial hierarchy
site = ifcopenshell.api.run(
"root.create_entity", ifc,
ifc_class="IfcSite",
name=site_name,
)
building = ifcopenshell.api.run(
"root.create_entity", ifc,
ifc_class="IfcBuilding",
name=building_name,
)
ifcopenshell.api.run(
"aggregate.assign_object", ifc,
relating_object=project,
products=[site],
)
ifcopenshell.api.run(
"aggregate.assign_object", ifc,
relating_object=site,
products=[building],
)
return ifc, site, building, body_ctx
class StoreyManager:
"""
Lazily creates IfcBuildingStorey entities as new level names are encountered.
Keeps storeys in insertion order so the IFC file is logically ordered.
Spatial containment is batched — call flush() after all elements are created
to write all IfcRelContainedInSpatialStructure / aggregate relationships at once.
"""
def __init__(self, ifc: ifcopenshell.file, building):
self.ifc = ifc
self.building = building
self._storeys: dict[str, object] = {} # level_name → IfcBuildingStorey
# Batched containment: storey_id → [element, ...]
self._contained: dict[int, list] = {}
# Batched aggregation (IfcSite etc.): storey_id → [element, ...]
self._aggregated: dict[int, list] = {}
def get_or_create(self, level_name: str):
"""Return existing storey or create a new one for this level name."""
if level_name not in self._storeys:
storey = ifcopenshell.api.run(
"root.create_entity", self.ifc,
ifc_class="IfcBuildingStorey",
name=level_name,
)
ifcopenshell.api.run(
"aggregate.assign_object", self.ifc,
relating_object=self.building,
products=[storey],
)
self._storeys[level_name] = storey
print(f" 🏢 Created storey: {level_name}")
return self._storeys[level_name]
def queue_contain(self, storey, element):
"""Queue an element for spatial containment (batched flush)."""
sid = storey.id()
if sid not in self._contained:
self._contained[sid] = []
self._contained[sid].append(element)
def queue_aggregate(self, storey, element):
"""Queue an element for aggregation under storey (e.g. IfcSite)."""
sid = storey.id()
if sid not in self._aggregated:
self._aggregated[sid] = []
self._aggregated[sid].append(element)
def flush(self):
"""Write all batched spatial containment and aggregation relationships."""
ifc = self.ifc
for sid, elements in self._contained.items():
storey = ifc.by_id(sid)
ifcopenshell.api.run(
"spatial.assign_container", ifc,
relating_structure=storey,
products=elements,
)
for sid, elements in self._aggregated.items():
storey = ifc.by_id(sid)
ifcopenshell.api.run(
"aggregate.assign_object", ifc,
relating_object=storey,
products=elements,
)
self._contained.clear()
self._aggregated.clear()
@property
def count(self) -> int:
return len(self._storeys)
@property
def names(self) -> list[str]:
return list(self._storeys.keys())