3 Commits

Author SHA1 Message Date
NLSA 0fb8ceb73a Update Readme and write file
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2026-03-19 14:10:27 +01:00
NLSA 09dc8eb8c8 displayvalue updates 2026-03-16 22:58:38 +01:00
NLSA 38e05ccc28 write file
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2026-03-13 15:48:54 +01:00
5 changed files with 206 additions and 140 deletions
+60 -24
View File
@@ -12,6 +12,7 @@ The exporter receives a Speckle model version, walks its object tree, and produc
- 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
## Pipeline Overview
@@ -33,7 +34,7 @@ Speckle Model
│ ├── Classify → IFC entity class
│ ├── Convert geometry → IfcPolygonalFaceSet
│ ├── Create IFC element + placement
│ ├── Write property sets
│ ├── Write property sets & quantities
│ └── Assign IFC type object
@@ -52,19 +53,19 @@ Speckle Model
| `utils/mapper.py` | Classifies Speckle objects into IFC entity types |
| `utils/geometry.py` | Converts Speckle meshes to IfcPolygonalFaceSet 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 |
@@ -81,23 +82,11 @@ Examples:
| `OST_PipeCurves` | `IfcPipeSegment` |
| `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
For typed Speckle objects, the `speckle_type` string is matched. Exact match is tried first, then longest-prefix match.
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` |
### 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.
@@ -109,9 +98,9 @@ Examples:
| `Plumbing Fixtures` | `IfcSanitaryTerminal` |
| `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 +125,11 @@ 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.
### 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,11 +138,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 |
## Quantities
Quantities follow the IFC standard naming convention: `Qto_<EntityType>BaseQuantities` and `Qto_<MaterialName>BaseQuantities`.
| 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 |
### Element Quantity Mapping
| 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` |
### Supported Entity Qto Sets
`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`
## IfcSpace (Rooms)
Revit Rooms (`OST_Rooms`) are exported as `IfcSpace` elements with special handling:
- **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
@@ -160,6 +186,16 @@ The exporter writes property sets matching Revit's native IFC export structure:
| `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 |
-27
View File
@@ -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
+36 -25
View File
@@ -162,26 +162,37 @@ 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
if not rep and not nested_instances:
@@ -205,12 +216,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!")
+8 -61
View File
@@ -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
# =============================================================================
@@ -119,44 +118,7 @@ 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",
}
# --- Display category name → IFC class (tertiary fallback) ---
# --- Display category name → IFC class (secondary fallback) ---
CATEGORY_MAP: dict[str, str] = {
"Walls": "IfcWall",
"Floors": "IfcSlab",
@@ -235,11 +197,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()
@@ -255,10 +212,9 @@ def classify(obj, category_name: str = "") -> str:
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:
@@ -275,16 +231,7 @@ def _classify_impl(obj, category_name: str) -> str:
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 +240,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:
+102 -3
View File
@@ -644,7 +644,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)
@@ -711,7 +711,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,
@@ -723,6 +723,103 @@ 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",
}
# 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:
@@ -731,10 +828,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)