Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fac5601b35 | |||
| cde3a58b08 | |||
| 682a21130f | |||
| 50e62020ef | |||
| 1681d756e8 | |||
| 0fb8ceb73a | |||
| 09dc8eb8c8 | |||
| 38e05ccc28 | |||
| f9193ee3a0 | |||
| 112f031608 |
@@ -1,6 +1,10 @@
|
||||
# Speckle to IFC 4.3 Exporter
|
||||
# Speckle-Revit to IFC 4.3 Exporter
|
||||
|
||||
A [Speckle Automate](https://automate.speckle.dev/) function that converts Speckle BIM models (primarily from Revit) into IFC 4.3 files using [ifcopenshell](https://ifcopenshell.org/).
|
||||
## 🚧 Project Status: WIP
|
||||
Hey there! This project is still under active development, so expect changes, bugs, and incomplete features.
|
||||
If you have any questions or suggestions, don’t hesitate to reach out at: **your@email.com**
|
||||
|
||||
A [Speckle Automate](https://automate.speckle.dev/) function that converts Speckle Revit models into IFC 4.3 files using [ifcopenshell](https://ifcopenshell.org/). This exporter is specifically designed for models sent to Speckle from Autodesk Revit and relies on Revit-specific object structures, categories, and parameters.
|
||||
|
||||
## What It Does
|
||||
|
||||
@@ -8,10 +12,13 @@ The exporter receives a Speckle model version, walks its object tree, and produc
|
||||
|
||||
- Correct IFC entity classification (IfcWall, IfcSlab, IfcColumn, etc.)
|
||||
- Tessellated geometry (IfcPolygonalFaceSet)
|
||||
- Curve geometry for Lines and Arcs (IfcGeometricCurveSet with IfcPolyline)
|
||||
- Material colours from Speckle render materials
|
||||
- Revit property sets (Common psets, instance/type parameters, material quantities)
|
||||
- IFC type objects (IfcWallType, IfcSlabType, etc.) shared across instances
|
||||
- Spatial structure (IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey)
|
||||
- IfcSpace elements aggregated under storeys with Room properties
|
||||
- Automatic skipping of analytical/energy categories (e.g. Energy Analysis, MEP Analytical, Solar Shading)
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
@@ -30,10 +37,10 @@ Speckle Model
|
||||
▼
|
||||
4. Traverse object tree
|
||||
│ For each leaf element:
|
||||
│ ├── Classify → IFC entity class
|
||||
│ ├── Convert geometry → IfcPolygonalFaceSet
|
||||
│ ├── Classify → IFC entity class (skip analytical categories)
|
||||
│ ├── Convert geometry → IfcPolygonalFaceSet or IfcGeometricCurveSet
|
||||
│ ├── Create IFC element + placement
|
||||
│ ├── Write property sets
|
||||
│ ├── Write property sets & quantities
|
||||
│ └── Assign IFC type object
|
||||
│
|
||||
▼
|
||||
@@ -50,21 +57,21 @@ Speckle Model
|
||||
| `main.py` | Entry point, orchestrates the full pipeline |
|
||||
| `utils/traversal.py` | Walks the Speckle Collection tree (Project > Level > Category > Element) |
|
||||
| `utils/mapper.py` | Classifies Speckle objects into IFC entity types |
|
||||
| `utils/geometry.py` | Converts Speckle meshes to IfcPolygonalFaceSet geometry |
|
||||
| `utils/geometry.py` | Converts Speckle meshes to IfcPolygonalFaceSet and Lines/Arcs to IfcGeometricCurveSet geometry |
|
||||
| `utils/instances.py` | Handles InstanceProxy objects with shared geometry (IfcMappedItem) |
|
||||
| `utils/properties.py` | Writes IFC property sets from Revit parameters |
|
||||
| `utils/properties.py` | Writes IFC property sets and quantities from Revit parameters |
|
||||
| `utils/type_manager.py` | Creates and caches IfcTypeObjects (IfcWallType, etc.) |
|
||||
| `utils/materials.py` | Maps Speckle render materials to IfcSurfaceStyle colours |
|
||||
| `utils/writer.py` | Creates the IFC file scaffold and manages storey creation |
|
||||
| `utils/config.py` | Project/site/building name configuration |
|
||||
| `utils/receiver.py` | Connects to Speckle server and receives model data (uses `.env`) |
|
||||
|
||||
## Mapping Logic
|
||||
|
||||
Classification of Speckle objects to IFC entity types follows a priority chain with three lookup tables. The first match wins.
|
||||
Classification of Speckle objects to IFC entity types follows a priority chain. The first match wins.
|
||||
|
||||
### Priority 1: `builtInCategory` (OST_ enum)
|
||||
|
||||
The most reliable source. Read from `obj.properties.builtInCategory`, which contains the Revit `BuiltInCategory` enum value. This is a direct Revit classification and maps unambiguously to IFC.
|
||||
The most reliable source. Read from `obj.properties.builtInCategory`, which contains the Revit `BuiltInCategory` enum value.
|
||||
|
||||
Examples:
|
||||
| builtInCategory | IFC Class |
|
||||
@@ -79,25 +86,23 @@ Examples:
|
||||
| `OST_CurtainWallPanels` | `IfcCurtainWall` |
|
||||
| `OST_DuctCurves` | `IfcDuctSegment` |
|
||||
| `OST_PipeCurves` | `IfcPipeSegment` |
|
||||
| `OST_PipeFitting` | `IfcPipeFitting` |
|
||||
| `OST_PlumbingEquipment` | `IfcSanitaryTerminal` |
|
||||
| `OST_Rebar` | `IfcReinforcingBar` |
|
||||
| `OST_StructConnections` | `IfcMechanicalFastener` |
|
||||
| `OST_LightingFixtures` | `IfcLightFixture` |
|
||||
| `OST_Furniture` | `IfcFurnishingElement` |
|
||||
| `OST_Rooms` | `IfcSpace` |
|
||||
|
||||
The full table covers ~70 Revit categories across Architectural, Structural, MEP (HVAC, Plumbing, Electrical), and Site/Civil disciplines.
|
||||
|
||||
### Priority 2: `speckle_type` prefix
|
||||
### Skipped Categories
|
||||
|
||||
For typed Speckle objects, the `speckle_type` string is matched. Exact match is tried first, then longest-prefix match.
|
||||
The following analytical/energy OST categories are automatically skipped (not exported to IFC):
|
||||
|
||||
Examples:
|
||||
| speckle_type | IFC Class |
|
||||
|---|---|
|
||||
| `Objects.BuiltElements.Wall` | `IfcWall` |
|
||||
| `Objects.BuiltElements.Floor` | `IfcSlab` |
|
||||
| `Objects.BuiltElements.Revit.RevitWall` | `IfcWall` |
|
||||
| `Objects.BuiltElements.Revit.RevitColumn` | `IfcColumn` |
|
||||
| `Objects.Geometry.Mesh` | `IfcBuildingElementProxy` |
|
||||
`OST_MEPLoadAreaSeparationLines`, `OST_EnergyAnalysisZones`, `OST_EnergyAnalysisSurface`, `OST_SolarShading`, `OST_MEPAnalyticalPipeSegments`, `OST_MEPAnalyticalDuctSegments`, `OST_MEPAnalyticalSpaces`, `OST_ElectricalConduitAnalyticalLines`, `OST_MEPLoadBoundaryLines`, `OST_FlowTerminalSeparationLines`
|
||||
|
||||
### Priority 3: Category name (display string)
|
||||
### Priority 2: Category name (display string)
|
||||
|
||||
The category name from the traversal context (the name of the parent Collection in the Speckle tree). Exact match first, then case-insensitive substring match.
|
||||
|
||||
@@ -107,11 +112,13 @@ Examples:
|
||||
| `Walls` | `IfcWall` |
|
||||
| `Structural Columns` | `IfcColumn` |
|
||||
| `Plumbing Fixtures` | `IfcSanitaryTerminal` |
|
||||
| `Structural Rebar` | `IfcReinforcingBar` |
|
||||
| `Structural Connections` | `IfcMechanicalFastener` |
|
||||
| `Lighting Fixtures` | `IfcLightFixture` |
|
||||
|
||||
### Priority 4: `obj.category` field
|
||||
### Priority 3: `obj.category` field
|
||||
|
||||
Same lookup as Priority 3, but using the object's own `category` attribute.
|
||||
Same lookup as Priority 2, but using the object's own `category` attribute.
|
||||
|
||||
### Fallback
|
||||
|
||||
@@ -136,7 +143,20 @@ Speckle `InstanceProxy` objects reference shared definition geometry via `defini
|
||||
- **Revit format**: `definitionId` is a 64-char hex hash; geometry is found by walking the object tree
|
||||
- **IFC format**: `definitionId` starts with `DEFINITION:`; geometry is in `definitionGeometry` collection
|
||||
|
||||
Performance optimisation: geometry is built once as an `IfcRepresentationMap`, then each instance references it via `IfcMappedItem` + `IfcCartesianTransformationOperator3DnonUniform`. This avoids duplicating vertex data across hundreds of identical elements (e.g. chairs, light fixtures, curtain wall panels).
|
||||
Performance optimisation: geometry is built once as an `IfcRepresentationMap`, then each instance references it via `IfcMappedItem` + `IfcCartesianTransformationOperator3DnonUniform`. This avoids duplicating vertex data across hundreds of identical elements.
|
||||
|
||||
### Curve Geometry (Path B3)
|
||||
|
||||
Objects whose `displayValue` contains `Objects.Geometry.Line` or `Objects.Geometry.Arc` items (and no meshes or instances) are exported as curve geometry:
|
||||
|
||||
- **Lines** → `IfcPolyline` with start and end points
|
||||
- **Arcs** → `IfcPolyline` approximated with 8 segments, sampled parametrically from the arc's plane origin, radius, and domain angles. Falls back to start/mid/end points if plane data is unavailable.
|
||||
|
||||
All curves are wrapped in an `IfcGeometricCurveSet` inside an `IfcShapeRepresentation` with `RepresentationType="GeometricCurveSet"`.
|
||||
|
||||
### Composite Objects (Path B2 — merged instances)
|
||||
|
||||
Objects like Windows and Doors may have multiple `InstanceProxy` items in their `displayValue` (e.g. frame, glass, sill). These are **not** separate IFC elements — all instance geometries are merged into a single `IfcShapeRepresentation` with combined `IfcMappedItem` entries, producing one IFC element per Speckle object.
|
||||
|
||||
## Property Sets
|
||||
|
||||
@@ -145,46 +165,44 @@ The exporter writes property sets matching Revit's native IFC export structure:
|
||||
| Property Set | Content |
|
||||
|---|---|
|
||||
| `Pset_<Entity>Common` | Standard IFC properties: Reference, IsExternal, LoadBearing, ThermalTransmittance |
|
||||
| `RVT_TypeParameters` | All Revit type parameters (written on the IfcTypeObject) |
|
||||
| `Pset_SpaceCommon` | Room-specific: Reference, RoomNumber, RoomName, Category (Occupant) |
|
||||
| `RVT_InstanceParameters` | All Revit instance parameters |
|
||||
| `RVT_Identity` | Family, Type, ElementId, BuiltInCategory |
|
||||
| `Qto_<MaterialName>` | Material quantities: area, volume, density |
|
||||
|
||||
## Getting Started
|
||||
## Quantities
|
||||
|
||||
### Prerequisites
|
||||
Quantities follow the IFC standard naming convention: `Qto_<EntityType>BaseQuantities` and `Qto_<MaterialName>BaseQuantities`.
|
||||
|
||||
- Python 3.11+
|
||||
- A Speckle account and project with a Revit model
|
||||
| Quantity Set | Content |
|
||||
|---|---|
|
||||
| `Qto_<EntityType>BaseQuantities` | Element-level quantities from Revit computed parameters (area, volume, length, width, height, perimeter) |
|
||||
| `Qto_SpaceBaseQuantities` | Room quantities: NetFloorArea, NetVolume |
|
||||
| `Qto_<MaterialName>BaseQuantities` | Per-material quantities: GrossArea, GrossVolume, Density |
|
||||
|
||||
### Setup
|
||||
### Element Quantity Mapping
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
# Windows
|
||||
.venv\Scripts\activate
|
||||
# macOS/Linux
|
||||
source .venv/bin/activate
|
||||
| IFC Quantity | Revit Parameter(s) |
|
||||
|---|---|
|
||||
| GrossArea | `HOST_AREA_COMPUTED` |
|
||||
| GrossVolume | `HOST_VOLUME_COMPUTED` |
|
||||
| Length | `CURVE_ELEM_LENGTH`, `INSTANCE_LENGTH_PARAM` |
|
||||
| Height | `WALL_USER_HEIGHT_PARAM`, `FAMILY_HEIGHT_PARAM`, `INSTANCE_HEAD_HEIGHT_PARAM` |
|
||||
| Width | `INSTANCE_WIDTH_PARAM`, `FURNITURE_WIDTH`, `FLOOR_ATTR_THICKNESS_PARAM` |
|
||||
| Perimeter | `HOST_PERIMETER_COMPUTED` |
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install .[dev]
|
||||
```
|
||||
### Supported Entity Qto Sets
|
||||
|
||||
### Running Locally
|
||||
`Qto_WallBaseQuantities`, `Qto_SlabBaseQuantities`, `Qto_ColumnBaseQuantities`, `Qto_BeamBaseQuantities`, `Qto_DoorBaseQuantities`, `Qto_WindowBaseQuantities`, `Qto_RoofBaseQuantities`, `Qto_CoveringBaseQuantities`, `Qto_RailingBaseQuantities`, `Qto_StairBaseQuantities`, `Qto_RampBaseQuantities`, `Qto_MemberBaseQuantities`, `Qto_FootingBaseQuantities`, `Qto_CurtainWallBaseQuantities`, `Qto_BuildingElementProxyBaseQuantities`, `Qto_PipeFittingBaseQuantities`, `Qto_SanitaryTerminalBaseQuantities`, `Qto_ReinforcingElementBaseQuantities`, `Qto_MechanicalFastenerBaseQuantities`
|
||||
|
||||
Configure your Speckle Automate credentials, then:
|
||||
## IfcSpace (Rooms)
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
Revit Rooms (`OST_Rooms`) are exported as `IfcSpace` elements with special handling:
|
||||
|
||||
### Deploying to Speckle Automate
|
||||
|
||||
1. [Create](https://automate.speckle.dev/) a new Speckle Automation
|
||||
2. Select your Speckle Project and Model
|
||||
3. Select this function
|
||||
4. Configure the inputs (file name, project/site/building names)
|
||||
5. Click Create Automation
|
||||
- **Spatial relationship**: Aggregated under `IfcBuildingStorey` via `IfcRelAggregates` (not contained)
|
||||
- **Naming**: Uses the Speckle object `name` attribute (not Family:Type which is "none:none" for rooms)
|
||||
- **IfcSpace.Name**: Set to `ROOM_NUMBER`
|
||||
- **IfcSpace.LongName**: Set to `ROOM_NAME`
|
||||
- **Geometry**: Converted from `displayValue` meshes like any other element
|
||||
|
||||
## Function Inputs
|
||||
|
||||
@@ -195,6 +213,25 @@ python main.py
|
||||
| `IFC_SITE_NAME` | Name for the IfcSite entity |
|
||||
| `IFC_BUILDING_NAME` | Name for the IfcBuilding entity |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
For local testing via `receiver.py`, configure a `.env` file:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `SPECKLE_SERVER_URL` | Speckle server URL (default: `https://app.speckle.systems`) |
|
||||
| `SPECKLE_TOKEN` | Personal access token for authentication |
|
||||
| `SPECKLE_PROJECT_ID` | Project (stream) ID |
|
||||
|
||||
## Testing
|
||||
|
||||
| Model Name | Revit Size | IFC Size | Conversion Time |
|
||||
|----------------------------------|------------|----------|-----------------|
|
||||
| Huge confidential model | 450 MB | 391 MB | 2h 30m |
|
||||
| Snowdon Towers (Architecture) | 93.2 MB | 118 MB | 8m 37s |
|
||||
| Speckle Tower | 51 MB | 45 MB | 3m |
|
||||
| Rac Basic Sample Model | 18.8 MB | 12 MB | 12s |
|
||||
|
||||
## Resources
|
||||
|
||||
- [Speckle Developer Docs](https://speckle.guide/dev/python.html)
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
"""Helper module for a simple speckle object tree flattening."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from specklepy.objects import Base
|
||||
|
||||
|
||||
def flatten_base(base: Base) -> Iterable[Base]:
|
||||
"""Flatten a base object into an iterable of bases.
|
||||
|
||||
This function recursively traverses the `elements` or `@elements` attribute of the
|
||||
base object, yielding each nested base object.
|
||||
|
||||
Args:
|
||||
base (Base): The base object to flatten.
|
||||
|
||||
Yields:
|
||||
Base: Each nested base object in the hierarchy.
|
||||
"""
|
||||
# Attempt to get the elements attribute, fallback to @elements if necessary
|
||||
elements = getattr(base, "elements", getattr(base, "@elements", None))
|
||||
|
||||
if elements is not None:
|
||||
for element in elements:
|
||||
yield from flatten_base(element)
|
||||
|
||||
yield base
|
||||
@@ -5,7 +5,7 @@ import ifcopenshell.api
|
||||
from utils.materials import MaterialManager
|
||||
from utils.traversal import traverse, print_tree
|
||||
from utils.mapper import classify, reset_caches as reset_mapper_caches
|
||||
from utils.geometry import mesh_to_ifc, get_display_instances, _make_placement
|
||||
from utils.geometry import mesh_to_ifc, get_display_instances, curves_to_ifc, _make_placement
|
||||
from utils.instances import is_instance, instance_to_ifc, build_definition_map, print_instance_stats, get_definition_object
|
||||
from utils.properties import write_properties, write_common_properties, build_element_name, get_element_tag, get_ifc_guid, reset_caches as reset_props_caches
|
||||
from utils.writer import create_ifc_scaffold, StoreyManager
|
||||
@@ -14,7 +14,7 @@ from utils.type_manager import TypeManager
|
||||
|
||||
SPATIAL_STRUCTURE_TYPES = {
|
||||
"IfcBuilding", "IfcBuildingStorey",
|
||||
"IfcSpace", "IfcExternalSpatialElement", "IfcSpatialZone",
|
||||
"IfcExternalSpatialElement", "IfcSpatialZone",
|
||||
"IfcGrid", "IfcAnnotation",
|
||||
}
|
||||
|
||||
@@ -106,11 +106,19 @@ def automate_function(
|
||||
|
||||
ifc_class = classify(obj, category_name)
|
||||
|
||||
if ifc_class is None:
|
||||
continue
|
||||
|
||||
if ifc_class in SPATIAL_STRUCTURE_TYPES:
|
||||
skipped_spatial += 1
|
||||
continue
|
||||
|
||||
name = build_element_name(obj)
|
||||
# IfcSpace uses the Speckle object name (e.g. "Rooms - Live/Work Unit 507")
|
||||
# instead of Family:Type (which is "none:none" for Revit rooms)
|
||||
if ifc_class == "IfcSpace":
|
||||
name = getattr(obj, "name", None) or build_element_name(obj)
|
||||
else:
|
||||
name = build_element_name(obj)
|
||||
storey = storey_manager.get_or_create(level_name)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -157,30 +165,52 @@ def automate_function(
|
||||
total += 1
|
||||
|
||||
# B2: Instance objects nested inside displayValue
|
||||
# Each becomes its own IFC element (same class as parent)
|
||||
# Use the parent object's name — the InstanceProxy has no meaningful name
|
||||
# All instances are parts of the SAME element (e.g. window frame + glass + sill)
|
||||
# Merge all into a single IFC element with combined geometry
|
||||
nested_instances = get_display_instances(obj)
|
||||
for inst in nested_instances:
|
||||
inst_rep, inst_placement = instance_to_ifc(
|
||||
ifc, body_context, inst, definition_map, scale=scale, material_manager=material_manager
|
||||
)
|
||||
if not inst_rep:
|
||||
no_geometry += 1
|
||||
continue
|
||||
inst_element = _create_element(
|
||||
ifc, ifc_class, name, inst_rep, inst_placement, storey,
|
||||
storey_manager=storey_manager,
|
||||
tag=get_element_tag(obj), guid=None,
|
||||
object_type=getattr(obj, "type", None),
|
||||
)
|
||||
write_properties(ifc, inst_element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(inst_element, obj, ifc_class)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
if nested_instances:
|
||||
mapped_items = []
|
||||
inst_placement = None
|
||||
for inst in nested_instances:
|
||||
inst_rep, inst_pl = instance_to_ifc(
|
||||
ifc, body_context, inst, definition_map, scale=scale, material_manager=material_manager
|
||||
)
|
||||
if inst_rep:
|
||||
mapped_items.extend(inst_rep.Items)
|
||||
if inst_placement is None:
|
||||
inst_placement = inst_pl
|
||||
if mapped_items:
|
||||
combined_rep = ifc.createIfcShapeRepresentation(
|
||||
ContextOfItems=body_context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=mapped_items,
|
||||
)
|
||||
element = _create_element(
|
||||
ifc, ifc_class, name, combined_rep, inst_placement, storey,
|
||||
storey_manager=storey_manager,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj),
|
||||
object_type=getattr(obj, "type", None),
|
||||
)
|
||||
write_properties(ifc, element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(element, obj, ifc_class)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
|
||||
# Track if neither path produced geometry
|
||||
# B3: Curve geometry (Lines, Arcs in displayValue)
|
||||
if not rep and not nested_instances:
|
||||
no_geometry += 1
|
||||
curve_rep, curve_placement = curves_to_ifc(ifc, body_context, obj, scale=scale, material_manager=material_manager)
|
||||
if curve_rep:
|
||||
element = _create_element(ifc, ifc_class, name, curve_rep, curve_placement, storey,
|
||||
storey_manager=storey_manager,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj),
|
||||
object_type=getattr(obj, "type", None),
|
||||
)
|
||||
write_properties(ifc, element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(element, obj, ifc_class)
|
||||
total += 1
|
||||
else:
|
||||
no_geometry += 1
|
||||
|
||||
if total % 100 == 0:
|
||||
print(f" ... processed {total} elements")
|
||||
@@ -200,12 +230,12 @@ def automate_function(
|
||||
|
||||
ifc.write(ifc_filename)
|
||||
print(f"\n💾 IFC file written: {ifc_filename}")
|
||||
# try:
|
||||
# automate_context.mark_run_success("Success! You can download the IF file below.")
|
||||
# automate_context.store_file_result(f"./{ifc_filename}")
|
||||
# except Exception as e:
|
||||
# print(f" ⚠️ Could not upload file result (network issue?): {e}")
|
||||
# automate_context.mark_run_failed(f"Something went wrong when storing file result. Exception detail: {e}")
|
||||
try:
|
||||
automate_context.mark_run_success("Success! You can download the IF file below.")
|
||||
automate_context.store_file_result(f"./{ifc_filename}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not upload file result (network issue?): {e}")
|
||||
automate_context.mark_run_failed(f"Something went wrong when storing file result. Exception detail: {e}")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Export complete!")
|
||||
@@ -250,8 +280,10 @@ def _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
element.ObjectPlacement = _make_placement(ifc, 0.0, 0.0, 0.0)
|
||||
|
||||
# Queue spatial assignment (batched flush at end for performance)
|
||||
# IfcSpace is a spatial structure element — must be decomposed (aggregated)
|
||||
# under its IfcBuildingStorey, not spatially contained.
|
||||
if storey_manager:
|
||||
if ifc_class == "IfcSite":
|
||||
if ifc_class in ("IfcSite", "IfcSpace"):
|
||||
storey_manager.queue_aggregate(storey, element)
|
||||
else:
|
||||
storey_manager.queue_contain(storey, element)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["speckle", "automate", "bim", "aec", "ifc", "export", "revit"]
|
||||
|
||||
dependencies = ["specklepy==3.2.4",
|
||||
dependencies = ["specklepy==3.1.0",
|
||||
"ifcopenshell==0.8.4.post1",
|
||||
"python-dotenv>=1.0.0",]
|
||||
|
||||
|
||||
+216
-26
@@ -11,6 +11,8 @@
|
||||
# for compact output — each vertex stored once, not once per face.
|
||||
# =============================================================================
|
||||
|
||||
import math
|
||||
|
||||
import ifcopenshell
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
@@ -53,34 +55,47 @@ def build_ifc_facesets(ifc, verts_scaled: list, face_groups: list) -> list:
|
||||
|
||||
# Validate faces and remap indices to deduplicated vertex list
|
||||
valid_faces = [] # list of (idx0+1, idx1+1, ...) tuples (1-based for IFC)
|
||||
vert_len = len(verts_scaled)
|
||||
for indices in face_groups:
|
||||
try:
|
||||
remapped = []
|
||||
seen_snaps = set()
|
||||
degenerate = False
|
||||
|
||||
for i in indices:
|
||||
i3 = i * 3
|
||||
x = verts_scaled[i3]
|
||||
y = verts_scaled[i3 + 1]
|
||||
z = verts_scaled[i3 + 2]
|
||||
key = (round(x * inv_tol), round(y * inv_tol), round(z * inv_tol))
|
||||
if key in seen_snaps:
|
||||
degenerate = True
|
||||
break
|
||||
seen_snaps.add(key)
|
||||
idx = snap_to_idx.get(key)
|
||||
if idx is None:
|
||||
idx = len(deduped_verts)
|
||||
snap_to_idx[key] = idx
|
||||
deduped_verts.append([x, y, z])
|
||||
remapped.append(idx + 1) # 1-based for IFC
|
||||
|
||||
if degenerate or len(remapped) < 3:
|
||||
continue
|
||||
valid_faces.append(remapped)
|
||||
except Exception:
|
||||
if indices is None:
|
||||
continue
|
||||
if not isinstance(indices, (list, tuple)):
|
||||
continue
|
||||
|
||||
remapped = []
|
||||
seen_snaps = set()
|
||||
degenerate = False
|
||||
invalid = False
|
||||
|
||||
for i in indices:
|
||||
if not isinstance(i, int):
|
||||
invalid = True
|
||||
break
|
||||
|
||||
i3 = i * 3
|
||||
if i3 < 0 or i3 + 2 >= vert_len:
|
||||
invalid = True
|
||||
break
|
||||
|
||||
x = verts_scaled[i3]
|
||||
y = verts_scaled[i3 + 1]
|
||||
z = verts_scaled[i3 + 2]
|
||||
key = (round(x * inv_tol), round(y * inv_tol), round(z * inv_tol))
|
||||
if key in seen_snaps:
|
||||
degenerate = True
|
||||
break
|
||||
seen_snaps.add(key)
|
||||
|
||||
idx = snap_to_idx.get(key)
|
||||
if idx is None:
|
||||
idx = len(deduped_verts)
|
||||
snap_to_idx[key] = idx
|
||||
deduped_verts.append([x, y, z])
|
||||
remapped.append(idx + 1) # 1-based for IFC
|
||||
|
||||
if invalid or degenerate or len(remapped) < 3:
|
||||
continue
|
||||
valid_faces.append(remapped)
|
||||
|
||||
if not valid_faces or not deduped_verts:
|
||||
return []
|
||||
@@ -245,6 +260,181 @@ def get_display_instances(obj: Base) -> list:
|
||||
return instances
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Curve detection & extraction (Lines, Arcs)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _is_line(item) -> bool:
|
||||
"""Detect Objects.Geometry.Line (but not Polyline)."""
|
||||
if item is None:
|
||||
return False
|
||||
st = _get(item, "speckle_type") or ""
|
||||
return "Line" in st and "Polyline" not in st
|
||||
|
||||
|
||||
def _is_arc(item) -> bool:
|
||||
"""Detect Objects.Geometry.Arc."""
|
||||
if item is None:
|
||||
return False
|
||||
st = _get(item, "speckle_type") or ""
|
||||
return "Arc" in st
|
||||
|
||||
|
||||
def get_display_curves(obj: Base) -> list:
|
||||
"""Extract Line and Arc objects from a DataObject's displayValue."""
|
||||
curves = []
|
||||
for key in ["displayValue", "@displayValue"]:
|
||||
display = _get(obj, key)
|
||||
if display is None:
|
||||
continue
|
||||
items = display if isinstance(display, list) else [display]
|
||||
for item in items:
|
||||
if _is_line(item) or _is_arc(item):
|
||||
curves.append(item)
|
||||
if curves:
|
||||
break
|
||||
return curves
|
||||
|
||||
|
||||
def _point_coords(pt, fallback_scale: float) -> tuple:
|
||||
"""Extract (x, y, z) from a Speckle Point, scaled to mm."""
|
||||
scale = _resolve_scale(pt, fallback_scale)
|
||||
x = float(_get(pt, "x") or 0.0) * scale
|
||||
y = float(_get(pt, "y") or 0.0) * scale
|
||||
z = float(_get(pt, "z") or 0.0) * scale
|
||||
return x, y, z
|
||||
|
||||
|
||||
def _arc_to_points(arc, scale: float, num_segments: int = 8) -> list:
|
||||
"""
|
||||
Approximate a Speckle Arc as a list of (x, y, z) points in mm.
|
||||
Uses plane origin (center), radius, and domain angles for parametric sampling.
|
||||
Falls back to start/mid/end points if plane data is missing.
|
||||
"""
|
||||
plane = _get(arc, "plane")
|
||||
radius = _get(arc, "radius")
|
||||
domain = _get(arc, "domain")
|
||||
|
||||
if not plane or not radius or not domain:
|
||||
points = []
|
||||
for key in ["startPoint", "midPoint", "endPoint"]:
|
||||
pt = _get(arc, key)
|
||||
if pt:
|
||||
points.append(_point_coords(pt, scale))
|
||||
return points if len(points) >= 2 else []
|
||||
|
||||
origin = _get(plane, "origin")
|
||||
xdir = _get(plane, "xdir")
|
||||
ydir = _get(plane, "ydir")
|
||||
|
||||
if not origin or not xdir or not ydir:
|
||||
points = []
|
||||
for key in ["startPoint", "midPoint", "endPoint"]:
|
||||
pt = _get(arc, key)
|
||||
if pt:
|
||||
points.append(_point_coords(pt, scale))
|
||||
return points if len(points) >= 2 else []
|
||||
|
||||
cx, cy, cz = _point_coords(origin, scale)
|
||||
# Direction vectors are unitless — do not scale
|
||||
dxx = float(_get(xdir, "x") or 0.0)
|
||||
dxy = float(_get(xdir, "y") or 0.0)
|
||||
dxz = float(_get(xdir, "z") or 0.0)
|
||||
dyx = float(_get(ydir, "x") or 0.0)
|
||||
dyy = float(_get(ydir, "y") or 0.0)
|
||||
dyz = float(_get(ydir, "z") or 0.0)
|
||||
|
||||
r = float(radius) * scale
|
||||
t_start = float(_get(domain, "start") or 0.0)
|
||||
t_end = float(_get(domain, "end") or 0.0)
|
||||
|
||||
points = []
|
||||
for i in range(num_segments + 1):
|
||||
t = t_start + (t_end - t_start) * i / num_segments
|
||||
cos_t = math.cos(t)
|
||||
sin_t = math.sin(t)
|
||||
x = cx + r * (cos_t * dxx + sin_t * dyx)
|
||||
y = cy + r * (cos_t * dxy + sin_t * dyy)
|
||||
z = cz + r * (cos_t * dxz + sin_t * dyz)
|
||||
points.append((x, y, z))
|
||||
return points
|
||||
|
||||
|
||||
def curves_to_ifc(
|
||||
ifc: ifcopenshell.file,
|
||||
body_context,
|
||||
obj: Base,
|
||||
scale: float = 0.001,
|
||||
material_manager=None,
|
||||
) -> tuple:
|
||||
"""
|
||||
Convert Speckle Line/Arc objects in displayValue to IFC curve geometry.
|
||||
Lines → IfcPolyline (2 points), Arcs → IfcPolyline (sampled points).
|
||||
Wrapped in IfcGeometricCurveSet.
|
||||
Returns (IfcShapeRepresentation, IfcLocalPlacement) or (None, None).
|
||||
"""
|
||||
curves = get_display_curves(obj)
|
||||
if not curves:
|
||||
return None, None
|
||||
|
||||
obj_scale = _resolve_scale(obj, scale)
|
||||
polylines = []
|
||||
all_points = []
|
||||
|
||||
for curve in curves:
|
||||
cs = _resolve_scale(curve, obj_scale)
|
||||
|
||||
if _is_line(curve):
|
||||
start = _get(curve, "start")
|
||||
end = _get(curve, "end")
|
||||
if not start or not end:
|
||||
continue
|
||||
p1 = _point_coords(start, cs)
|
||||
p2 = _point_coords(end, cs)
|
||||
all_points.extend([p1, p2])
|
||||
polylines.append([p1, p2])
|
||||
|
||||
elif _is_arc(curve):
|
||||
pts = _arc_to_points(curve, cs)
|
||||
if len(pts) >= 2:
|
||||
all_points.extend(pts)
|
||||
polylines.append(pts)
|
||||
|
||||
if not polylines or not all_points:
|
||||
return None, None
|
||||
|
||||
# Compute origin from all curve points
|
||||
xs = [p[0] for p in all_points]
|
||||
ys = [p[1] for p in all_points]
|
||||
zs = [p[2] for p in all_points]
|
||||
ox = (min(xs) + max(xs)) / 2.0
|
||||
oy = (min(ys) + max(ys)) / 2.0
|
||||
oz = min(zs)
|
||||
|
||||
# Build IfcPolylines offset from origin
|
||||
ifc_polylines = []
|
||||
for pts in polylines:
|
||||
ifc_points = [
|
||||
ifc.createIfcCartesianPoint([p[0] - ox, p[1] - oy, p[2] - oz])
|
||||
for p in pts
|
||||
]
|
||||
ifc_polylines.append(ifc.createIfcPolyline(ifc_points))
|
||||
|
||||
if not ifc_polylines:
|
||||
return None, None
|
||||
|
||||
curve_set = ifc.createIfcGeometricCurveSet(ifc_polylines)
|
||||
|
||||
rep = ifc.createIfcShapeRepresentation(
|
||||
ContextOfItems=body_context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="GeometricCurveSet",
|
||||
Items=[curve_set],
|
||||
)
|
||||
placement = _make_placement(ifc, ox, oy, oz)
|
||||
return rep, placement
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Face decoding
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
+33
-62
@@ -4,9 +4,8 @@
|
||||
#
|
||||
# Strategy (priority order):
|
||||
# 1. builtInCategory (OST_ enum from properties.builtInCategory) — most reliable
|
||||
# 2. speckle_type prefix match — for typed Speckle objects
|
||||
# 3. category_name string (traversal context) — display name fallback
|
||||
# 4. IfcBuildingElementProxy — last resort
|
||||
# 2. category_name string (traversal context) — display name fallback
|
||||
# 3. IfcBuildingElementProxy — last resort
|
||||
#
|
||||
# builtInCategory values: https://www.revitapidocs.com/2019/ba1c5b30-242f-5fdc-8ea9-ec3b61e6e722.htm
|
||||
# =============================================================================
|
||||
@@ -63,6 +62,7 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
"OST_StructuralStiffener": "IfcMember",
|
||||
"OST_StructuralTruss": "IfcMember",
|
||||
"OST_StructuralConnectionModel": "IfcMechanicalFastener",
|
||||
"OST_StructConnections": "IfcMechanicalFastener",
|
||||
"OST_Rebar": "IfcReinforcingBar",
|
||||
"OST_FabricAreas": "IfcReinforcingMesh",
|
||||
"OST_FabricReinforcement": "IfcReinforcingMesh",
|
||||
@@ -82,6 +82,7 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
"OST_PipeAccessory": "IfcPipeSegment",
|
||||
"OST_FlexPipeCurves": "IfcPipeSegment",
|
||||
"OST_PlumbingFixtures": "IfcSanitaryTerminal",
|
||||
"OST_PlumbingEquipment": "IfcSanitaryTerminal",
|
||||
"OST_Sprinklers": "IfcFireSuppressionTerminal",
|
||||
|
||||
# MEP - Electrical
|
||||
@@ -119,44 +120,22 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
# --- speckle_type → IFC class (secondary lookup) ---
|
||||
SPECKLE_TYPE_MAP: dict[str, str] = {
|
||||
"Objects.BuiltElements.Wall": "IfcWall",
|
||||
"Objects.BuiltElements.Floor": "IfcSlab",
|
||||
"Objects.BuiltElements.Roof": "IfcRoof",
|
||||
"Objects.BuiltElements.Column": "IfcColumn",
|
||||
"Objects.BuiltElements.Beam": "IfcBeam",
|
||||
"Objects.BuiltElements.Brace": "IfcMember",
|
||||
"Objects.BuiltElements.Duct": "IfcDuctSegment",
|
||||
"Objects.BuiltElements.Pipe": "IfcPipeSegment",
|
||||
"Objects.BuiltElements.Wire": "IfcCableCarrierSegment",
|
||||
"Objects.BuiltElements.Opening": "IfcOpeningElement",
|
||||
"Objects.BuiltElements.Room": "IfcSpace",
|
||||
"Objects.BuiltElements.Ceiling": "IfcCovering",
|
||||
"Objects.BuiltElements.Stair": "IfcStair",
|
||||
"Objects.BuiltElements.Ramp": "IfcRamp",
|
||||
"Objects.BuiltElements.Foundation": "IfcFooting",
|
||||
"Objects.BuiltElements.Grid": "IfcGrid",
|
||||
"Objects.BuiltElements.Level": "IfcBuildingStorey",
|
||||
"Objects.BuiltElements.Revit.RevitWall": "IfcWall",
|
||||
"Objects.BuiltElements.Revit.RevitFloor": "IfcSlab",
|
||||
"Objects.BuiltElements.Revit.RevitRoof": "IfcRoof",
|
||||
"Objects.BuiltElements.Revit.RevitColumn": "IfcColumn",
|
||||
"Objects.BuiltElements.Revit.RevitBeam": "IfcBeam",
|
||||
"Objects.BuiltElements.Revit.RevitBrace": "IfcMember",
|
||||
"Objects.BuiltElements.Revit.RevitDuct": "IfcDuctSegment",
|
||||
"Objects.BuiltElements.Revit.RevitPipe": "IfcPipeSegment",
|
||||
"Objects.BuiltElements.Revit.RevitRoom": "IfcSpace",
|
||||
"Objects.BuiltElements.Revit.RevitStair": "IfcStair",
|
||||
"Objects.BuiltElements.Revit.RevitRailing": "IfcRailing",
|
||||
"Objects.BuiltElements.Revit.RevitCeiling": "IfcCovering",
|
||||
"Objects.BuiltElements.Revit.RevitTopography": "IfcGeographicElement",
|
||||
"Objects.BuiltElements.Revit.RevitElementType": "IfcBuildingElementProxy",
|
||||
"Objects.Geometry.Mesh": "IfcBuildingElementProxy",
|
||||
"Objects.Geometry.Brep": "IfcBuildingElementProxy",
|
||||
# --- OST categories to skip entirely (analytical / energy / separation lines) ---
|
||||
SKIP_CATEGORIES: set[str] = {
|
||||
"OST_MEPLoadAreaSeparationLines",
|
||||
"OST_EnergyAnalysisZones",
|
||||
"OST_EnergyAnalysisSurface",
|
||||
"OST_SolarShading",
|
||||
"OST_MEPAnalyticalPipeSegments",
|
||||
"OST_MEPAnalyticalDuctSegments",
|
||||
"OST_MEPAnalyticalSpaces",
|
||||
"OST_ElectricalConduitAnalyticalLines",
|
||||
"OST_MEPLoadBoundaryLines",
|
||||
"OST_FlowTerminalSeparationLines",
|
||||
}
|
||||
|
||||
# --- Display category name → IFC class (tertiary fallback) ---
|
||||
|
||||
# --- Display category name → IFC class (secondary fallback) ---
|
||||
CATEGORY_MAP: dict[str, str] = {
|
||||
"Walls": "IfcWall",
|
||||
"Floors": "IfcSlab",
|
||||
@@ -184,10 +163,13 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
"Furniture Systems": "IfcFurnishingElement",
|
||||
"Casework": "IfcFurnishingElement",
|
||||
"Plumbing Fixtures": "IfcSanitaryTerminal",
|
||||
"Plumbing Equipment": "IfcSanitaryTerminal",
|
||||
"Electrical Fixtures": "IfcElectricAppliance",
|
||||
"Lighting Fixtures": "IfcLightFixture",
|
||||
"Mechanical Equipment": "IfcUnitaryEquipment",
|
||||
"Electrical Equipment": "IfcElectricDistributionBoard",
|
||||
"Structural Rebar": "IfcReinforcingBar",
|
||||
"Structural Connections": "IfcMechanicalFastener",
|
||||
"Structural Foundations": "IfcFooting",
|
||||
"Foundation Slabs": "IfcSlab",
|
||||
"Topography": "IfcGeographicElement",
|
||||
@@ -235,11 +217,6 @@ def _get_builtin_category(obj) -> str | None:
|
||||
return result
|
||||
|
||||
|
||||
# Pre-computed: sorted prefixes longest-first for early exit on prefix match
|
||||
_SPECKLE_PREFIXES: list[tuple[str, str]] = sorted(
|
||||
SPECKLE_TYPE_MAP.items(), key=lambda x: len(x[0]), reverse=True
|
||||
)
|
||||
|
||||
# Pre-computed lowercase category map for substring matching
|
||||
_CATEGORY_MAP_LOWER: list[tuple[str, str]] = [
|
||||
(k.lower(), v) for k, v in CATEGORY_MAP.items()
|
||||
@@ -249,16 +226,15 @@ _CATEGORY_MAP_LOWER: list[tuple[str, str]] = [
|
||||
_classify_cache: dict[tuple, str] = {}
|
||||
|
||||
|
||||
def classify(obj, category_name: str = "") -> str:
|
||||
def classify(obj, category_name: str = "") -> str | None:
|
||||
"""
|
||||
Determine the IFC class for a Speckle object.
|
||||
|
||||
Priority:
|
||||
1. properties.builtInCategory (OST_ enum) — definitive Revit classification
|
||||
2. speckle_type prefix match
|
||||
3. category_name from traversal context (display string)
|
||||
4. obj.category field
|
||||
5. IfcBuildingElementProxy fallback
|
||||
2. category_name from traversal context (display string)
|
||||
3. obj.category field
|
||||
4. IfcBuildingElementProxy fallback
|
||||
"""
|
||||
cache_key = (id(obj), category_name)
|
||||
if cache_key in _classify_cache:
|
||||
@@ -269,22 +245,17 @@ def classify(obj, category_name: str = "") -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _classify_impl(obj, category_name: str) -> str:
|
||||
# 1. builtInCategory — most reliable, direct Revit enum
|
||||
def _classify_impl(obj, category_name: str) -> str | None:
|
||||
# 0. Skip analytical / energy / separation-line categories
|
||||
bic = _get_builtin_category(obj)
|
||||
if bic and bic in SKIP_CATEGORIES:
|
||||
return None
|
||||
|
||||
# 1. builtInCategory — most reliable, direct Revit enum
|
||||
if bic and bic in BUILTIN_CATEGORY_MAP:
|
||||
return BUILTIN_CATEGORY_MAP[bic]
|
||||
|
||||
# 2. speckle_type — exact match first, then longest-prefix match
|
||||
speckle_type = getattr(obj, "speckle_type", "") or ""
|
||||
if speckle_type:
|
||||
if speckle_type in SPECKLE_TYPE_MAP:
|
||||
return SPECKLE_TYPE_MAP[speckle_type]
|
||||
for prefix, ifc_class in _SPECKLE_PREFIXES:
|
||||
if speckle_type.startswith(prefix):
|
||||
return ifc_class
|
||||
|
||||
# 3. category_name from traversal context — exact match first
|
||||
# 2. category_name from traversal context — exact match first
|
||||
if category_name:
|
||||
if category_name in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[category_name]
|
||||
@@ -293,7 +264,7 @@ def _classify_impl(obj, category_name: str) -> str:
|
||||
if key_lower in cat_lower:
|
||||
return ifc_class
|
||||
|
||||
# 4. obj.category field
|
||||
# 3. obj.category field
|
||||
obj_category = getattr(obj, "category", None)
|
||||
if obj_category and isinstance(obj_category, str):
|
||||
if obj_category in CATEGORY_MAP:
|
||||
|
||||
+205
-3
@@ -53,6 +53,10 @@ COMMON_PSET: dict[str, str] = {
|
||||
"IfcOpeningElement": "Pset_OpeningElementCommon",
|
||||
"IfcPlate": "Pset_PlateCommon",
|
||||
"IfcGeographicElement": "Pset_SiteCommon",
|
||||
"IfcPipeFitting": "Pset_PipeFittingTypeCommon",
|
||||
"IfcSanitaryTerminal": "Pset_SanitaryTerminalTypeCommon",
|
||||
"IfcReinforcingBar": "Pset_ReinforcingBarBendingsBECCommon",
|
||||
"IfcMechanicalFastener": "Pset_MechanicalFastenerTypeCommon",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -424,9 +428,104 @@ def write_common_pset(ifc, element, obj: Base, ifc_class: str, category_name: st
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# IfcSpace-specific: set Name, LongName, Category, and BaseQuantities
|
||||
if ifc_class == "IfcSpace":
|
||||
_write_space_properties(ifc, element, obj, ifc_props)
|
||||
|
||||
_write_pset(ifc, element, pset_name, ifc_props)
|
||||
|
||||
|
||||
def _write_space_properties(ifc, element, obj: Base, ifc_props: list):
|
||||
"""
|
||||
Set IfcSpace attributes and BaseQuantities from Revit Room parameters.
|
||||
|
||||
Uses internalDefinitionName to find values:
|
||||
ROOM_NUMBER → IfcSpace.Name + Pset_SpaceCommon.Reference
|
||||
ROOM_NAME → IfcSpace.LongName
|
||||
Occupant → Pset_SpaceCommon.Category
|
||||
ROOM_AREA → Qto_SpaceBaseQuantities.NetFloorArea
|
||||
ROOM_VOLUME → Qto_SpaceBaseQuantities.NetVolume
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
inst_params = _safe_get(params, "Instance Parameters", {})
|
||||
|
||||
# --- Room Number → IfcSpace.Name + Pset_SpaceCommon.Reference ---
|
||||
room_number = _param_value(inst_params, "ROOM_NUMBER")
|
||||
if room_number:
|
||||
room_number = str(room_number).strip()
|
||||
element.Name = room_number
|
||||
# Replace any existing Reference in ifc_props
|
||||
ifc_props[:] = [p for p in ifc_props if p.Name != "Reference"]
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", room_number)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
# Also add as explicit RoomNumber in the pset
|
||||
p = _make_prop(ifc, "RoomNumber", "IfcLabel", room_number)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# --- Room Name → IfcSpace.LongName + Pset_SpaceCommon.RoomName ---
|
||||
room_name = _param_value(inst_params, "ROOM_NAME")
|
||||
if not room_name:
|
||||
# Fallback to the Speckle object's own name
|
||||
room_name = getattr(obj, "name", None)
|
||||
if room_name:
|
||||
room_name = str(room_name).strip()
|
||||
try:
|
||||
element.LongName = room_name
|
||||
except AttributeError:
|
||||
pass
|
||||
p = _make_prop(ifc, "RoomName", "IfcLabel", room_name)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# --- Occupant → Pset_SpaceCommon.Category ---
|
||||
occupant = _param_value(inst_params, "Occupant")
|
||||
if occupant:
|
||||
p = _make_prop(ifc, "Category", "IfcLabel", str(occupant).strip())
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# --- Area & Volume → Qto_SpaceBaseQuantities ---
|
||||
quantities = []
|
||||
|
||||
area_val = _param_value(inst_params, "ROOM_AREA")
|
||||
if area_val is not None:
|
||||
try:
|
||||
q = ifc.create_entity(
|
||||
"IfcQuantityArea",
|
||||
Name="NetFloorArea",
|
||||
AreaValue=float(area_val),
|
||||
)
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
volume_val = _param_value(inst_params, "ROOM_VOLUME")
|
||||
if volume_val is not None:
|
||||
try:
|
||||
q = ifc.create_entity(
|
||||
"IfcQuantityVolume",
|
||||
Name="NetVolume",
|
||||
VolumeValue=float(volume_val),
|
||||
)
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if quantities:
|
||||
try:
|
||||
qto = ifcopenshell.api.run(
|
||||
"pset.add_qto", ifc,
|
||||
product=element,
|
||||
name="Qto_SpaceBaseQuantities",
|
||||
)
|
||||
qto.Quantities = quantities
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Qto_SpaceBaseQuantities: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pset_EnvironmentalImpactIndicators (always written, Reference = TypeName)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -549,7 +648,7 @@ def write_material_quantities(ifc, element, obj: Base):
|
||||
Source: properties."Material Quantities".<MaterialName>.{area, volume, density,
|
||||
materialName, materialClass, materialCategory}
|
||||
|
||||
Each material produces one IfcElementQuantity named "Qto_<MaterialName>" with:
|
||||
Each material produces one IfcElementQuantity named "Qto_<MaterialName>BaseQuantities" with:
|
||||
- GrossArea (IfcQuantityArea)
|
||||
- GrossVolume (IfcQuantityVolume)
|
||||
- Density (IfcPropertySingleValue — no standard IFC quantity type)
|
||||
@@ -616,7 +715,7 @@ def write_material_quantities(ifc, element, obj: Base):
|
||||
continue
|
||||
|
||||
# Create IfcElementQuantity and link via IfcRelDefinesByProperties
|
||||
qto_name = f"Qto_{mat_name}"
|
||||
qto_name = f"Qto_{mat_name}BaseQuantities"
|
||||
try:
|
||||
qto = ifcopenshell.api.run(
|
||||
"pset.add_qto", ifc,
|
||||
@@ -628,6 +727,107 @@ def write_material_quantities(ifc, element, obj: Base):
|
||||
print(f" ⚠️ {qto_name}: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Qto_<EntityType>BaseQuantities — standard element-level quantities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# IFC entity → Qto name (only entities with standard Qto sets)
|
||||
_ENTITY_QTO_NAME: dict[str, str] = {
|
||||
"IfcWall": "Qto_WallBaseQuantities",
|
||||
"IfcWallStandardCase": "Qto_WallBaseQuantities",
|
||||
"IfcSlab": "Qto_SlabBaseQuantities",
|
||||
"IfcColumn": "Qto_ColumnBaseQuantities",
|
||||
"IfcBeam": "Qto_BeamBaseQuantities",
|
||||
"IfcDoor": "Qto_DoorBaseQuantities",
|
||||
"IfcWindow": "Qto_WindowBaseQuantities",
|
||||
"IfcRoof": "Qto_RoofBaseQuantities",
|
||||
"IfcCovering": "Qto_CoveringBaseQuantities",
|
||||
"IfcRailing": "Qto_RailingBaseQuantities",
|
||||
"IfcStair": "Qto_StairBaseQuantities",
|
||||
"IfcRamp": "Qto_RampBaseQuantities",
|
||||
"IfcMember": "Qto_MemberBaseQuantities",
|
||||
"IfcFooting": "Qto_FootingBaseQuantities",
|
||||
"IfcCurtainWall": "Qto_CurtainWallBaseQuantities",
|
||||
"IfcBuildingElementProxy": "Qto_BuildingElementProxyBaseQuantities",
|
||||
"IfcPipeFitting": "Qto_PipeFittingBaseQuantities",
|
||||
"IfcSanitaryTerminal": "Qto_SanitaryTerminalBaseQuantities",
|
||||
"IfcReinforcingBar": "Qto_ReinforcingElementBaseQuantities",
|
||||
"IfcMechanicalFastener": "Qto_MechanicalFastenerBaseQuantities",
|
||||
}
|
||||
|
||||
# IFC quantity name → (IFC entity type, value attribute, [Revit param fallbacks])
|
||||
# First matching Revit param wins for each quantity name.
|
||||
_ELEMENT_QUANTITY_DEFS: list[tuple[str, str, str, list[str]]] = [
|
||||
("GrossArea", "IfcQuantityArea", "AreaValue", ["HOST_AREA_COMPUTED"]),
|
||||
("GrossVolume", "IfcQuantityVolume", "VolumeValue", ["HOST_VOLUME_COMPUTED"]),
|
||||
("Length", "IfcQuantityLength", "LengthValue", [
|
||||
"CURVE_ELEM_LENGTH", "INSTANCE_LENGTH_PARAM",
|
||||
]),
|
||||
("Height", "IfcQuantityLength", "LengthValue", [
|
||||
"WALL_USER_HEIGHT_PARAM", "FAMILY_HEIGHT_PARAM",
|
||||
"INSTANCE_HEAD_HEIGHT_PARAM",
|
||||
]),
|
||||
("Width", "IfcQuantityLength", "LengthValue", [
|
||||
"INSTANCE_WIDTH_PARAM", "FURNITURE_WIDTH",
|
||||
"FLOOR_ATTR_THICKNESS_PARAM",
|
||||
]),
|
||||
("Perimeter", "IfcQuantityLength", "LengthValue", [
|
||||
"HOST_PERIMETER_COMPUTED",
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
def write_element_quantities(ifc, element, obj: Base, ifc_class: str = ""):
|
||||
"""
|
||||
Write Qto_<EntityType>BaseQuantities from Revit computed instance parameters.
|
||||
|
||||
Reads HOST_AREA_COMPUTED, HOST_VOLUME_COMPUTED, CURVE_ELEM_LENGTH,
|
||||
FURNITURE_WIDTH, FAMILY_HEIGHT_PARAM, etc.
|
||||
IfcSpace is handled separately in _write_space_properties.
|
||||
"""
|
||||
if ifc_class == "IfcSpace":
|
||||
return # Already handled by Qto_SpaceBaseQuantities
|
||||
|
||||
qto_name = _ENTITY_QTO_NAME.get(ifc_class)
|
||||
if not qto_name:
|
||||
return
|
||||
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
inst_params = _safe_get(params, "Instance Parameters", {})
|
||||
if not inst_params:
|
||||
return
|
||||
|
||||
quantities = []
|
||||
|
||||
for qty_name, ifc_entity, value_attr, revit_params in _ELEMENT_QUANTITY_DEFS:
|
||||
val = None
|
||||
for internal_name in revit_params:
|
||||
val = _param_value(inst_params, internal_name)
|
||||
if val is not None:
|
||||
break
|
||||
if val is None:
|
||||
continue
|
||||
try:
|
||||
q = ifc.create_entity(ifc_entity, Name=qty_name, **{value_attr: float(val)})
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not quantities:
|
||||
return
|
||||
|
||||
try:
|
||||
qto = ifcopenshell.api.run(
|
||||
"pset.add_qto", ifc,
|
||||
product=element,
|
||||
name=qto_name,
|
||||
)
|
||||
qto.Quantities = quantities
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {qto_name}: {e}")
|
||||
|
||||
|
||||
def write_properties(ifc, element, obj: Base, ifc_class: str = "", category_name: str = ""):
|
||||
"""
|
||||
Write all property sets for an IFC element, matching Revit native IFC export structure:
|
||||
@@ -636,10 +836,12 @@ def write_properties(ifc, element, obj: Base, ifc_class: str = "", category_name
|
||||
3. RVT_TypeParameters — all remaining Revit type parameters
|
||||
4. RVT_InstanceParameters — all remaining Revit instance parameters
|
||||
5. RVT_Identity — family, type, elementId, builtInCategory
|
||||
6. Qto_<MaterialName> — material quantities (area, volume, density)
|
||||
6. Qto_<EntityType>BaseQuantities — element-level quantities (area, volume, length)
|
||||
7. Qto_<MaterialName>BaseQuantities — material quantities (area, volume, density)
|
||||
"""
|
||||
write_common_pset(ifc, element, obj, ifc_class, category_name)
|
||||
write_revit_params(ifc, element, obj)
|
||||
write_element_quantities(ifc, element, obj, ifc_class)
|
||||
write_material_quantities(ifc, element, obj)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user