bim-twin-viewer/backend/app/services/ifc_parser.py
warnason 50b55cee42 Add database auto-creation and fix IFC parser flush logic
- Create tables on application startup via lifespan event
- Flush project before creating elements (generate project_id)
- Flush each element before creating properties (generate element_id)
- Add IfcWallStandardCase to parsed element types
- Include FZK-Haus sample IFC file for testing
- Tested: 58 elements, 3601 properties parsed successfully
2026-04-20 18:52:27 +02:00

128 lines
4 KiB
Python

"""Service for parsing IFC files and storing elements in the database."""
import tempfile
from pathlib import Path
import ifcopenshell
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.element import Element, Project, Property
async def parse_ifc_file(
filename: str,
content: bytes,
db: AsyncSession,
) -> Project:
"""
Parse an IFC file and persist its structure to the database.
Extracts building elements (walls, doors, slabs, windows, etc.)
along with their property sets into the relational model.
"""
# Write to temp file (ifcopenshell needs a file path)
with tempfile.NamedTemporaryFile(suffix=".ifc", delete=False) as tmp:
tmp.write(content)
tmp_path = Path(tmp.name)
try:
ifc = ifcopenshell.open(str(tmp_path))
finally:
tmp_path.unlink()
# Create project record
project = Project(
name=_extract_project_name(ifc, filename),
filename=filename,
description=_extract_project_description(ifc),
ifc_schema=ifc.schema,
)
db.add(project)
await db.flush()
# Extract elements
element_types = [
"IfcWall", "IfcWallStandardCase", "IfcDoor", "IfcWindow", "IfcSlab",
"IfcColumn", "IfcBeam", "IfcStair", "IfcRoof",
"IfcSpace", "IfcFurnishingElement", "IfcBuildingElementProxy",
]
for ifc_type in element_types:
for ifc_element in ifc.by_type(ifc_type):
storey = _get_storey(ifc_element)
element = Element(
project_id=project.id,
global_id=ifc_element.GlobalId,
ifc_type=ifc_element.is_a(),
name=ifc_element.Name or "",
description=ifc_element.Description or "",
storey=storey,
)
db.add(element)
await db.flush()
# Extract property sets
for pset_name, props in _get_properties(ifc_element).items():
for prop_name, prop_value in props.items():
prop = Property(
element_id=element.id,
pset_name=pset_name,
name=prop_name,
value=str(prop_value) if prop_value is not None else "",
)
db.add(prop)
await db.commit()
await db.refresh(project)
return project
def _extract_project_name(ifc, fallback_filename: str) -> str:
"""Try to get the project name from the IFC file."""
try:
ifc_project = ifc.by_type("IfcProject")[0]
if ifc_project.Name:
return ifc_project.Name
except (IndexError, AttributeError):
pass
return Path(fallback_filename).stem
def _extract_project_description(ifc) -> str:
"""Try to get the project description from the IFC file."""
try:
ifc_project = ifc.by_type("IfcProject")[0]
return ifc_project.Description or ""
except (IndexError, AttributeError):
return ""
def _get_storey(element) -> str:
"""Determine which building storey an element belongs to."""
try:
for rel in element.ContainedInStructure:
if rel.RelatingStructure.is_a("IfcBuildingStorey"):
return rel.RelatingStructure.Name or ""
except AttributeError:
pass
return ""
def _get_properties(element) -> dict[str, dict[str, any]]:
"""Extract all property sets from an IFC element."""
result = {}
try:
for definition in element.IsDefinedBy:
if definition.is_a("IfcRelDefinesByProperties"):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet"):
props = {}
for prop in pset.HasProperties:
if prop.is_a("IfcPropertySingleValue"):
value = prop.NominalValue.wrappedValue if prop.NominalValue else None
props[prop.Name] = value
result[pset.Name] = props
except AttributeError:
pass
return result