bim-twin-viewer/backend/app/services/ifc_parser.py
warnason 520d55259f
Some checks are pending
CI / backend-lint-and-test (push) Waiting to run
Initial project scaffold: FastAPI backend + Vue.js frontend
- FastAPI with async SQLAlchemy models for IFC elements
- IFC file upload and parsing via IfcOpenShell
- REST API for projects, elements, and properties
- Vue.js 3 frontend shell with Three.js dependency
- Docker Compose for full-stack local development
- PostgreSQL 16 as database
- CI pipeline for Forgejo Actions
- Project documentation and API overview
2026-04-20 11:21:30 +02:00

127 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)
# Extract elements
element_types = [
"IfcWall", "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)
# 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