update materials and instances
This commit is contained in:
@@ -6,30 +6,46 @@ A [Speckle Automate](https://automate.speckle.dev/) function that converts Speck
|
|||||||
|
|
||||||
The exporter receives a Speckle model version, walks its nested collection tree, and produces a standards-compliant IFC 4.3 file. Each Speckle object becomes an IFC element with:
|
The exporter receives a Speckle model version, walks its nested collection tree, and produces a standards-compliant IFC 4.3 file. Each Speckle object becomes an IFC element with:
|
||||||
|
|
||||||
- Correct IFC entity classification read from `_properties.Attributes.type`
|
- Correct IFC entity classification read from `properties.Attributes.type`
|
||||||
- Tessellated geometry (IfcPolygonalFaceSet)
|
- Tessellated geometry (IfcPolygonalFaceSet) from Mesh, Brep, or BrepX objects
|
||||||
- Material colours from Speckle render materials
|
- Material colours from `renderMaterialProxies` applied as IfcSurfaceStyle
|
||||||
- All property sets cloned from `_properties.Property Sets`
|
- All property sets cloned from `properties.Property Sets`
|
||||||
- All quantity sets cloned from `_properties.Quantities`
|
- All quantity sets cloned from `properties.Quantities` (supports both `{name, units, value}` dicts and plain numeric values)
|
||||||
- IFC type objects created from `_properties.Element Type Attributes`
|
- IFC type objects from `properties.Element Type Attributes` and/or `properties.Element Type Property Sets`
|
||||||
- Building storeys derived from `_properties.Building Storey`
|
- Building storeys derived from `properties.Building Storey`
|
||||||
- Spatial structure (IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey)
|
- Spatial structure (IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey)
|
||||||
|
|
||||||
|
Objects that serve as instance definition geometry sources are automatically skipped during export — their geometry is shared via IfcRepresentationMap.
|
||||||
|
|
||||||
## Object Structure
|
## Object Structure
|
||||||
|
|
||||||
The exporter expects Speckle objects with the following `_properties` layout:
|
The exporter expects Speckle `DataObject` elements with a `properties` dict:
|
||||||
|
|
||||||
```
|
```
|
||||||
_properties
|
properties
|
||||||
├── Attributes → IFC element attributes (type, GlobalId, Name, Tag, etc.)
|
├── Attributes → IFC element attributes (type, GlobalId, Name, Tag, etc.)
|
||||||
├── Property Sets → dict of {pset_name: {prop_name: value}}
|
├── Property Sets → {pset_name: {prop_name: value}}
|
||||||
├── Quantities → dict of {qto_name: {qty_name: {name, units, value}}}
|
├── Quantities → {qto_name: {qty_name: value_or_dict}}
|
||||||
├── Building Storey → string, used for storey assignment
|
├── Building Storey → string, used for storey assignment
|
||||||
├── Element Type Attributes → type class, Name, GlobalId (creates IfcTypeObject)
|
├── Element Type Attributes → (optional) type class, Name, GlobalId (creates IfcTypeObject)
|
||||||
└── Element Type Property Sets → property sets written on the IfcTypeObject
|
└── Element Type Property Sets → (optional) property sets written on the IfcTypeObject
|
||||||
```
|
```
|
||||||
|
|
||||||
The nested collection tree is expected as: `Root Collection > Collection > ... > Object`
|
The nested collection tree is expected as: `Root Collection > Collection > ... > DataObject`
|
||||||
|
|
||||||
|
### Type Object Handling
|
||||||
|
|
||||||
|
Two formats are supported for creating IFC type objects:
|
||||||
|
|
||||||
|
- **Format A** — `Element Type Attributes` contains explicit type info (`type`, `Name`, `GlobalId`, etc.) and `Element Type Property Sets` contains the type's property sets.
|
||||||
|
- **Format B** — Only `Element Type Property Sets` exists (no `Element Type Attributes`). The type class is derived from the element class (e.g. `IfcColumn` → `IfcColumnType`).
|
||||||
|
|
||||||
|
### Quantity Formats
|
||||||
|
|
||||||
|
Quantities support two value formats:
|
||||||
|
|
||||||
|
- **Dict format**: `{'name': 'Length', 'units': 'Millimetre', 'value': 3000}` — unit is used to select the correct IFC quantity type (IfcQuantityLength, IfcQuantityArea, IfcQuantityVolume, etc.)
|
||||||
|
- **Plain format**: `{'Length': 3000, 'GrossVolume': 0.27}` — quantity type is inferred from name keywords (e.g. "Length" → IfcQuantityLength, "Area" → IfcQuantityArea, "Volume" → IfcQuantityVolume). Falls back to IfcQuantityCount if no keyword matches.
|
||||||
|
|
||||||
## Pipeline Overview
|
## Pipeline Overview
|
||||||
|
|
||||||
@@ -40,26 +56,30 @@ Speckle Model
|
|||||||
1. Receive version (specklepy)
|
1. Receive version (specklepy)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
2. Build definition map (for instance geometry reuse)
|
2. Build definition map (for instance geometry reuse + definition source detection)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
3. Create IFC scaffold (Project → Site → Building)
|
3. Create IFC scaffold (Project → Site → Building)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
4. Traverse collection tree
|
4. Initialize material manager (parse renderMaterialProxies)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
5. Traverse collection tree
|
||||||
│ For each leaf element:
|
│ For each leaf element:
|
||||||
│ ├── Classify → IFC entity class (from _properties.Attributes.type)
|
│ ├── Skip spatial structure types and definition geometry sources
|
||||||
│ ├── Convert geometry → IfcPolygonalFaceSet
|
│ ├── Classify → IFC entity class (from properties.Attributes.type)
|
||||||
|
│ ├── Convert geometry → IfcPolygonalFaceSet (with material colours)
|
||||||
│ ├── Create IFC element + placement
|
│ ├── Create IFC element + placement
|
||||||
│ ├── Clone all properties & quantities
|
│ ├── Clone all properties & quantities
|
||||||
│ ├── Assign to Building Storey (from _properties.Building Storey)
|
│ ├── Assign to Building Storey (from properties.Building Storey)
|
||||||
│ └── Assign IFC type object (from Element Type Attributes)
|
│ └── Assign IFC type object
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
5. Flush spatial containment & type relationships
|
6. Flush spatial containment & type relationships
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
6. Write .ifc file
|
7. Write .ifc file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Module Structure
|
## Module Structure
|
||||||
@@ -67,19 +87,20 @@ Speckle Model
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `main.py` | Entry point, orchestrates the full pipeline |
|
| `main.py` | Entry point, orchestrates the full pipeline |
|
||||||
| `utils/traversal.py` | Walks the Speckle collection tree (Root > Collection* > Object) |
|
| `utils/helpers.py` | Shared utilities: safe attribute access (`_get`) and unit scale constants |
|
||||||
| `utils/mapper.py` | Reads IFC entity class from `_properties.Attributes.type` |
|
| `utils/traversal.py` | Walks the Speckle collection tree (Root > Collection* > DataObject) |
|
||||||
| `utils/geometry.py` | Converts Speckle meshes to IfcPolygonalFaceSet geometry |
|
| `utils/mapper.py` | Reads IFC entity class from `properties.Attributes.type` |
|
||||||
|
| `utils/geometry.py` | Converts Speckle Mesh/Brep/BrepX geometry to IfcPolygonalFaceSet |
|
||||||
| `utils/instances.py` | Handles InstanceProxy objects with shared geometry (IfcMappedItem) |
|
| `utils/instances.py` | Handles InstanceProxy objects with shared geometry (IfcMappedItem) |
|
||||||
| `utils/properties.py` | Clones all properties, quantities, and attributes into IFC entities |
|
| `utils/properties.py` | Clones all properties, quantities, and attributes into IFC entities |
|
||||||
| `utils/type_manager.py` | Creates and caches IfcTypeObjects from Element Type Attributes |
|
| `utils/type_manager.py` | Creates and caches IfcTypeObjects, supports both explicit and derived type classes |
|
||||||
| `utils/materials.py` | Maps Speckle render materials to IfcSurfaceStyle colours |
|
| `utils/materials.py` | Maps Speckle render materials to IfcSurfaceStyle colours |
|
||||||
| `utils/writer.py` | Creates the IFC file scaffold and manages storey creation |
|
| `utils/writer.py` | Creates the IFC file scaffold and manages storey creation |
|
||||||
| `utils/receiver.py` | Standalone Speckle model receiver utility |
|
| `utils/receiver.py` | Standalone Speckle model receiver utility |
|
||||||
|
|
||||||
## Classification
|
## Classification
|
||||||
|
|
||||||
IFC entity classification is read directly from `_properties.Attributes.type` on each object. For example, an object with `Attributes.type = "IfcWall"` becomes an `IfcWall` element. If the field is missing, the object falls back to `IfcBuildingElementProxy`.
|
IFC entity classification is read from `properties.Attributes.type` on each object. For example, `Attributes.type = "IfcWall"` produces an `IfcWall` element. Falls back to `IfcBuildingElementProxy` if missing.
|
||||||
|
|
||||||
For instance proxy objects without their own type, the exporter looks up the definition object's `Attributes.type`.
|
For instance proxy objects without their own type, the exporter looks up the definition object's `Attributes.type`.
|
||||||
|
|
||||||
@@ -89,19 +110,24 @@ All properties are cloned generically — no source-application-specific logic:
|
|||||||
|
|
||||||
| Source | IFC Target |
|
| Source | IFC Target |
|
||||||
|--------|------------|
|
|--------|------------|
|
||||||
| `_properties.Attributes` | Element attributes: GlobalId, Name, Tag, ObjectType, Description, PredefinedType |
|
| `properties.Attributes` | Element attributes: GlobalId, Name, Tag, ObjectType, Description, PredefinedType |
|
||||||
| `_properties.Property Sets.*` | IfcPropertySet per sub-dict (e.g. `Pset_WallCommon` → IfcPropertySet with IfcPropertySingleValue entries) |
|
| `properties.Property Sets.*` | IfcPropertySet per sub-dict (e.g. `Pset_WallCommon` → IfcPropertySingleValue entries) |
|
||||||
| `_properties.Quantities.*` | IfcElementQuantity per sub-dict, with automatic unit detection (mm → IfcQuantityLength, m² → IfcQuantityArea, m³ → IfcQuantityVolume) |
|
| `properties.Quantities.*` | IfcElementQuantity per sub-dict, with automatic unit detection (mm → IfcQuantityLength, m² → IfcQuantityArea, m³ → IfcQuantityVolume) and name-based inference (Length, Width, Height, Area, Volume, Weight) |
|
||||||
| `_properties.Element Type Attributes` | Shared IfcTypeObject (e.g. IfcWallType), cached by GlobalId |
|
| `properties.Element Type Attributes` | Shared IfcTypeObject (e.g. IfcWallType), cached by GlobalId |
|
||||||
| `_properties.Element Type Property Sets` | Property sets on the IfcTypeObject |
|
| `properties.Element Type Property Sets` | Property sets on the IfcTypeObject |
|
||||||
|
|
||||||
Property values are auto-typed: `bool` → IfcBoolean, `int` → IfcInteger, `float` → IfcReal, `str` → IfcLabel, `list` → comma-joined IfcLabel.
|
Property values are auto-typed: `bool` → IfcBoolean, `int` → IfcInteger, `float` → IfcReal, `str` → IfcLabel, `list` → comma-joined IfcLabel.
|
||||||
|
|
||||||
## Geometry Handling
|
## Geometry Handling
|
||||||
|
|
||||||
### Direct Meshes (Path B1)
|
### Supported Geometry Types
|
||||||
|
|
||||||
Objects with `displayValue` containing Mesh objects are converted directly:
|
The exporter handles three geometry types found in `displayValue`:
|
||||||
|
|
||||||
|
- **Mesh** — converted directly (vertices + faces)
|
||||||
|
- **Brep / BrepX** — recursively resolved to their inner tessellated mesh representation via nested `displayValue`
|
||||||
|
|
||||||
|
### Conversion Steps
|
||||||
|
|
||||||
1. Extract vertices and faces from each mesh in `displayValue`
|
1. Extract vertices and faces from each mesh in `displayValue`
|
||||||
2. Scale vertices to millimetres based on the mesh's unit declaration
|
2. Scale vertices to millimetres based on the mesh's unit declaration
|
||||||
@@ -113,6 +139,20 @@ Objects with `displayValue` containing Mesh objects are converted directly:
|
|||||||
|
|
||||||
Speckle `InstanceProxy` objects reference shared definition geometry via `definitionId`. 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.
|
Speckle `InstanceProxy` objects reference shared definition geometry via `definitionId`. 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.
|
||||||
|
|
||||||
|
## Material Handling
|
||||||
|
|
||||||
|
Materials are read from `root.renderMaterialProxies` and applied as `IfcSurfaceStyle` on geometry items. Each proxy contains a `RenderMaterial` (name, diffuse colour as ARGB packed int, opacity) and a list of object references.
|
||||||
|
|
||||||
|
The material resolution uses a multi-strategy lookup since `renderMaterialProxies.objects` references different IDs depending on the source format:
|
||||||
|
|
||||||
|
| Format | What `objects` references | Resolution strategy |
|
||||||
|
|--------|--------------------------|-------------------|
|
||||||
|
| **IFC format** (speckleifc) | Mesh applicationIds directly | Direct lookup by mesh applicationId |
|
||||||
|
| **Grasshopper format** | Inner InstanceProxy applicationIds | Map via definitionId → material |
|
||||||
|
| **Direct mesh/BrepX** | Parent DataObject applicationIds | Fall back to parent object's applicationId |
|
||||||
|
|
||||||
|
IFC styles are created lazily (only when actually assigned to geometry) to avoid orphaned IfcSurfaceStyle entities.
|
||||||
|
|
||||||
## Function Inputs
|
## Function Inputs
|
||||||
|
|
||||||
| Input | Description |
|
| Input | Description |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from utils.traversal import traverse, print_tree
|
|||||||
from utils.mapper import classify
|
from utils.mapper import classify
|
||||||
from utils.geometry import mesh_to_ifc, get_display_instances, _make_placement
|
from utils.geometry import mesh_to_ifc, get_display_instances, _make_placement
|
||||||
from utils.instances import (
|
from utils.instances import (
|
||||||
is_instance, instance_to_ifc, build_definition_map,
|
is_instance, is_definition_source, instance_to_ifc, build_definition_map,
|
||||||
print_instance_stats, get_definition_object,
|
print_instance_stats, get_definition_object,
|
||||||
)
|
)
|
||||||
from utils.properties import (
|
from utils.properties import (
|
||||||
@@ -86,6 +86,7 @@ def automate_function(
|
|||||||
)
|
)
|
||||||
storey_manager = StoreyManager(ifc, building)
|
storey_manager = StoreyManager(ifc, building)
|
||||||
material_manager = MaterialManager(ifc, base)
|
material_manager = MaterialManager(ifc, base)
|
||||||
|
material_manager.build_definition_material_map(definition_map)
|
||||||
type_manager = TypeManager(ifc)
|
type_manager = TypeManager(ifc)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -106,6 +107,10 @@ def automate_function(
|
|||||||
skipped_spatial += 1
|
skipped_spatial += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Skip objects that serve as instance definition geometry sources
|
||||||
|
if is_definition_source(obj, definition_map):
|
||||||
|
continue
|
||||||
|
|
||||||
# Get building storey from properties
|
# Get building storey from properties
|
||||||
storey_name = get_building_storey(obj)
|
storey_name = get_building_storey(obj)
|
||||||
storey = storey_manager.get_or_create(storey_name)
|
storey = storey_manager.get_or_create(storey_name)
|
||||||
@@ -207,6 +212,7 @@ def automate_function(
|
|||||||
print(f" Storeys created : {storey_manager.count}")
|
print(f" Storeys created : {storey_manager.count}")
|
||||||
print(f" Levels : {', '.join(storey_manager.names)}")
|
print(f" Levels : {', '.join(storey_manager.names)}")
|
||||||
print_instance_stats()
|
print_instance_stats()
|
||||||
|
material_manager.print_stats()
|
||||||
print(f"{'=' * 60}\n")
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
def _create_element(ifc, ifc_class, name, rep, placement, storey,
|
def _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||||
|
|||||||
+28
-43
@@ -13,16 +13,7 @@
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
from utils.helpers import _get, MM_SCALES as _UNIT_SCALES
|
||||||
|
|
||||||
# Scale factors → MILLIMETRES (IFC file is declared as mm)
|
|
||||||
_UNIT_SCALES = {
|
|
||||||
"mm": 1.0, "millimeter": 1.0, "millimeters": 1.0,
|
|
||||||
"cm": 10.0, "centimeter": 10.0, "centimeters": 10.0,
|
|
||||||
"m": 1000.0, "meter": 1000.0, "meters": 1000.0,
|
|
||||||
"ft": 304.8, "foot": 304.8, "feet": 304.8,
|
|
||||||
"in": 25.4, "inch": 25.4, "inches": 25.4,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -97,30 +88,6 @@ def build_ifc_facesets(ifc, verts_scaled: list, face_groups: list) -> list:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
# Safe data access helpers
|
|
||||||
# --------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
def _get(obj, key, default=None):
|
|
||||||
"""
|
|
||||||
Safe access for specklepy Base objects.
|
|
||||||
Tries attribute access first, then bracket access.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
val = getattr(obj, key, None)
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
val = obj[key]
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def unwrap_chunks(raw) -> list:
|
def unwrap_chunks(raw) -> list:
|
||||||
"""
|
"""
|
||||||
Flatten a Speckle data array into a plain Python list of numbers.
|
Flatten a Speckle data array into a plain Python list of numbers.
|
||||||
@@ -371,14 +338,21 @@ def mesh_to_ifc(
|
|||||||
if not meshes:
|
if not meshes:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
# Parent object's applicationId — used as fallback for material lookup
|
||||||
|
# when inner meshes (e.g. from BrepX) don't have their own applicationId
|
||||||
|
obj_app_id = _get(obj, "applicationId")
|
||||||
|
|
||||||
obj_scale = _resolve_scale(obj, scale)
|
obj_scale = _resolve_scale(obj, scale)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Pass 1: unpack vertices once per mesh, collect all scaled coords
|
# Pass 1: unpack and scale vertices once per mesh, compute origin
|
||||||
# to compute world origin. Cache (verts, ms) for Pass 2.
|
# incrementally without accumulating all vertices in memory.
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
mesh_cache = [] # [(verts_list, ms, scaled)] or None per mesh
|
mesh_cache = [] # [(verts_list, ms, scaled)] or None per mesh
|
||||||
all_scaled = []
|
xmin = ymin = zmin = float("inf")
|
||||||
|
xmax = ymax = float("-inf")
|
||||||
|
has_verts = False
|
||||||
|
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
raw_verts = _get(mesh, "vertices") or []
|
raw_verts = _get(mesh, "vertices") or []
|
||||||
verts = unwrap_chunks(raw_verts if isinstance(raw_verts, list) else list(raw_verts))
|
verts = unwrap_chunks(raw_verts if isinstance(raw_verts, list) else list(raw_verts))
|
||||||
@@ -386,15 +360,25 @@ def mesh_to_ifc(
|
|||||||
mesh_cache.append(None)
|
mesh_cache.append(None)
|
||||||
continue
|
continue
|
||||||
ms = _resolve_scale(mesh, obj_scale)
|
ms = _resolve_scale(mesh, obj_scale)
|
||||||
# Pre-scale vertices once, reuse in Pass 2
|
|
||||||
scaled = [float(v) * ms for v in verts]
|
scaled = [float(v) * ms for v in verts]
|
||||||
mesh_cache.append((verts, ms, scaled))
|
mesh_cache.append((verts, ms, scaled))
|
||||||
all_scaled.extend(scaled)
|
has_verts = True
|
||||||
|
|
||||||
if not all_scaled:
|
# Update bounding box from this mesh's scaled vertices
|
||||||
|
for i in range(0, len(scaled) - 2, 3):
|
||||||
|
x, y, z = scaled[i], scaled[i + 1], scaled[i + 2]
|
||||||
|
if x < xmin: xmin = x
|
||||||
|
if x > xmax: xmax = x
|
||||||
|
if y < ymin: ymin = y
|
||||||
|
if y > ymax: ymax = y
|
||||||
|
if z < zmin: zmin = z
|
||||||
|
|
||||||
|
if not has_verts:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
ox, oy, oz = compute_origin(all_scaled)
|
ox = (xmin + xmax) / 2.0
|
||||||
|
oy = (ymin + ymax) / 2.0
|
||||||
|
oz = zmin
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Pass 2: one faceset per mesh — reuse cached verts, only unpack faces
|
# Pass 2: one faceset per mesh — reuse cached verts, only unpack faces
|
||||||
@@ -414,7 +398,7 @@ def mesh_to_ifc(
|
|||||||
try:
|
try:
|
||||||
face_groups = decode_faces(faces_raw)
|
face_groups = decode_faces(faces_raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ Face decode error: {e}")
|
print(f" Warning: Face decode error: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Offset pre-scaled vertices relative to origin (flat list, no tuples)
|
# Offset pre-scaled vertices relative to origin (flat list, no tuples)
|
||||||
@@ -431,8 +415,9 @@ def mesh_to_ifc(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Apply material style to every faceset of this mesh
|
# Apply material style to every faceset of this mesh
|
||||||
|
# Inner meshes (from BrepX) may lack applicationId — fall back to parent's
|
||||||
if material_manager:
|
if material_manager:
|
||||||
mesh_app_id = _get(mesh, "applicationId")
|
mesh_app_id = _get(mesh, "applicationId") or obj_app_id
|
||||||
if mesh_app_id:
|
if mesh_app_id:
|
||||||
for fs in mesh_facesets:
|
for fs in mesh_facesets:
|
||||||
material_manager.apply_to_item(fs, str(mesh_app_id))
|
material_manager.apply_to_item(fs, str(mesh_app_id))
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# helpers.py
|
||||||
|
# Shared utilities used across the exporter modules.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _get(obj, key, default=None):
|
||||||
|
"""
|
||||||
|
Safe access for specklepy Base objects, dicts, or any hybrid.
|
||||||
|
Tries attribute access first, then bracket access.
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
return default
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
try:
|
||||||
|
val = getattr(obj, key, None)
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
val = obj[key]
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
# Scale factors → MILLIMETRES (IFC file is declared as mm)
|
||||||
|
MM_SCALES = {
|
||||||
|
"mm": 1.0, "millimeter": 1.0, "millimeters": 1.0,
|
||||||
|
"cm": 10.0, "centimeter": 10.0, "centimeters": 10.0,
|
||||||
|
"m": 1000.0, "meter": 1000.0, "meters": 1000.0,
|
||||||
|
"ft": 304.8, "foot": 304.8, "feet": 304.8,
|
||||||
|
"in": 25.4, "inch": 25.4, "inches": 25.4,
|
||||||
|
}
|
||||||
+94
-35
@@ -21,8 +21,10 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import ifcopenshell.api
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
from utils.geometry import _get, unwrap_chunks, decode_faces, _UNIT_SCALES, build_ifc_facesets, _get_shared
|
from utils.helpers import _get, MM_SCALES
|
||||||
|
from utils.geometry import unwrap_chunks, decode_faces, build_ifc_facesets, _get_shared
|
||||||
|
|
||||||
|
|
||||||
def is_instance(obj) -> bool:
|
def is_instance(obj) -> bool:
|
||||||
@@ -40,15 +42,18 @@ def build_definition_map(root: Base) -> dict:
|
|||||||
Build a unified definition map that handles both formats.
|
Build a unified definition map that handles both formats.
|
||||||
|
|
||||||
Returns dict with keys:
|
Returns dict with keys:
|
||||||
"by_id" : {obj_id_lower[:32] → object} for Revit format
|
"by_id" : {obj_id_lower[:32] → object} for Revit format
|
||||||
"by_app_id" : {applicationId_lower → object} for Revit format
|
"by_app_id" : {applicationId_lower → object} for Revit format
|
||||||
"ifc_proxies" : {"DEFINITION:xxx" → proxy} for IFC format
|
"ifc_proxies" : {"DEFINITION:xxx" → proxy} for IFC format
|
||||||
"ifc_meshes" : {meshAppId → Mesh} for IFC format
|
"ifc_meshes" : {meshAppId → Mesh} for IFC format
|
||||||
|
"definition_sources": set of applicationId (lowercase) that are definition
|
||||||
|
geometry sources — these should be skipped during export
|
||||||
"""
|
"""
|
||||||
by_id = {}
|
by_id = {}
|
||||||
by_app_id = {}
|
by_app_id = {}
|
||||||
ifc_proxies = {}
|
ifc_proxies = {}
|
||||||
ifc_meshes = {}
|
ifc_meshes = {}
|
||||||
|
definition_sources = set() # applicationIds used as definition geometry (skip during export)
|
||||||
|
|
||||||
# --- Walk entire tree for Revit format ---
|
# --- Walk entire tree for Revit format ---
|
||||||
_collect_all(root, by_id, by_app_id, depth=0)
|
_collect_all(root, by_id, by_app_id, depth=0)
|
||||||
@@ -61,6 +66,11 @@ def build_definition_map(root: Base) -> dict:
|
|||||||
if app_id:
|
if app_id:
|
||||||
ifc_proxies[app_id] = proxy # original case (for IFC format)
|
ifc_proxies[app_id] = proxy # original case (for IFC format)
|
||||||
ifc_proxies[app_id.lower()] = proxy # lowercase (for Revit format)
|
ifc_proxies[app_id.lower()] = proxy # lowercase (for Revit format)
|
||||||
|
# Collect all objects referenced by this proxy as definition sources
|
||||||
|
object_ids = _get(proxy, "objects") or []
|
||||||
|
for oid in (object_ids if isinstance(object_ids, list) else [object_ids]):
|
||||||
|
if oid:
|
||||||
|
definition_sources.add(str(oid).lower())
|
||||||
|
|
||||||
elements = _get(root, "elements") or _get(root, "@elements") or []
|
elements = _get(root, "elements") or _get(root, "@elements") or []
|
||||||
for child in (elements if isinstance(elements, list) else []):
|
for child in (elements if isinstance(elements, list) else []):
|
||||||
@@ -75,12 +85,14 @@ def build_definition_map(root: Base) -> dict:
|
|||||||
print(f" Objects indexed by appId: {len(by_app_id)}")
|
print(f" Objects indexed by appId: {len(by_app_id)}")
|
||||||
print(f" IFC definition proxies: {len(ifc_proxies)}")
|
print(f" IFC definition proxies: {len(ifc_proxies)}")
|
||||||
print(f" IFC definition meshes: {len(ifc_meshes)}")
|
print(f" IFC definition meshes: {len(ifc_meshes)}")
|
||||||
|
print(f" Definition sources: {len(definition_sources)}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"by_id": by_id,
|
"by_id": by_id,
|
||||||
"by_app_id": by_app_id,
|
"by_app_id": by_app_id,
|
||||||
"ifc_proxies": ifc_proxies,
|
"ifc_proxies": ifc_proxies,
|
||||||
"ifc_meshes": ifc_meshes,
|
"ifc_meshes": ifc_meshes,
|
||||||
|
"definition_sources": definition_sources,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -117,11 +129,14 @@ def _collect_all(obj, by_id: dict, by_app_id: dict, depth: int):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
def _get_revit_meshes(definition_id: str, definition_map: dict) -> list:
|
def _get_revit_meshes(definition_id: str, definition_map: dict) -> tuple:
|
||||||
"""
|
"""
|
||||||
Revit format:
|
Revit format:
|
||||||
definitionId (64-char hex) → InstanceDefinitionProxy.applicationId
|
definitionId (64-char hex) → InstanceDefinitionProxy.applicationId
|
||||||
proxy.objects[0] is a UUID applicationId → find mesh by applicationId
|
proxy.objects[0] is a UUID applicationId → find mesh by applicationId
|
||||||
|
|
||||||
|
Returns (meshes, app_ids) where app_ids are all applicationIds encountered
|
||||||
|
in the resolution chain (definition objects, geometry objects) for material fallback.
|
||||||
"""
|
"""
|
||||||
from utils.geometry import get_display_meshes
|
from utils.geometry import get_display_meshes
|
||||||
|
|
||||||
@@ -129,19 +144,34 @@ def _get_revit_meshes(definition_id: str, definition_map: dict) -> list:
|
|||||||
ifc_proxies = definition_map.get("ifc_proxies", {})
|
ifc_proxies = definition_map.get("ifc_proxies", {})
|
||||||
proxy = ifc_proxies.get(definition_id) or ifc_proxies.get(definition_id.lower())
|
proxy = ifc_proxies.get(definition_id) or ifc_proxies.get(definition_id.lower())
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
# Step 2: get the mesh applicationIds from proxy.objects
|
# Step 2: get the mesh applicationIds from proxy.objects
|
||||||
object_ids = _get(proxy, "objects") or []
|
object_ids = _get(proxy, "objects") or []
|
||||||
if not isinstance(object_ids, list):
|
if not isinstance(object_ids, list):
|
||||||
object_ids = list(object_ids)
|
object_ids = list(object_ids)
|
||||||
|
|
||||||
# Step 3: look up each mesh by applicationId
|
# Step 3: look up each mesh by applicationId, collecting all encountered app IDs
|
||||||
by_app_id = definition_map.get("by_app_id", {})
|
by_app_id = definition_map.get("by_app_id", {})
|
||||||
meshes = []
|
meshes = []
|
||||||
|
encountered_app_ids = []
|
||||||
for oid in object_ids:
|
for oid in object_ids:
|
||||||
obj = by_app_id.get(str(oid).lower())
|
obj = by_app_id.get(str(oid).lower())
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
|
# Collect this object's applicationId
|
||||||
|
obj_aid = _get(obj, "applicationId")
|
||||||
|
if obj_aid:
|
||||||
|
encountered_app_ids.append(str(obj_aid))
|
||||||
|
# Also collect applicationIds from displayValue items (BrepX, etc.)
|
||||||
|
for key in ["displayValue", "@displayValue", "_displayValue"]:
|
||||||
|
display = _get(obj, key)
|
||||||
|
if display:
|
||||||
|
items = display if isinstance(display, list) else [display]
|
||||||
|
for item in items:
|
||||||
|
item_aid = _get(item, "applicationId")
|
||||||
|
if item_aid:
|
||||||
|
encountered_app_ids.append(str(item_aid))
|
||||||
|
break
|
||||||
# The found object may itself be a mesh, or contain displayValue meshes
|
# The found object may itself be a mesh, or contain displayValue meshes
|
||||||
found_meshes = get_display_meshes(obj)
|
found_meshes = get_display_meshes(obj)
|
||||||
if found_meshes:
|
if found_meshes:
|
||||||
@@ -149,20 +179,21 @@ def _get_revit_meshes(definition_id: str, definition_map: dict) -> list:
|
|||||||
else:
|
else:
|
||||||
# It IS the mesh directly
|
# It IS the mesh directly
|
||||||
meshes.append(obj)
|
meshes.append(obj)
|
||||||
return meshes
|
return meshes, encountered_app_ids
|
||||||
|
|
||||||
|
|
||||||
def _get_ifc_meshes(definition_id: str, definition_map: dict) -> list:
|
def _get_ifc_meshes(definition_id: str, definition_map: dict) -> tuple:
|
||||||
"""
|
"""
|
||||||
IFC format: definitionId = "DEFINITION:224058_mat0"
|
IFC format: definitionId = "DEFINITION:224058_mat0"
|
||||||
Look up proxy → objects list → meshes from ifc_meshes dict.
|
Look up proxy → objects list → meshes from ifc_meshes dict.
|
||||||
|
Returns (meshes, []) — no extra app_ids needed, mesh applicationIds match directly.
|
||||||
"""
|
"""
|
||||||
ifc_proxies = definition_map.get("ifc_proxies", {})
|
ifc_proxies = definition_map.get("ifc_proxies", {})
|
||||||
ifc_meshes = definition_map.get("ifc_meshes", {})
|
ifc_meshes = definition_map.get("ifc_meshes", {})
|
||||||
|
|
||||||
proxy = ifc_proxies.get(definition_id)
|
proxy = ifc_proxies.get(definition_id)
|
||||||
if proxy is None:
|
if proxy is None:
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
object_ids = _get(proxy, "objects") or []
|
object_ids = _get(proxy, "objects") or []
|
||||||
result = []
|
result = []
|
||||||
@@ -170,7 +201,7 @@ def _get_ifc_meshes(definition_id: str, definition_map: dict) -> list:
|
|||||||
mesh = ifc_meshes.get(str(oid))
|
mesh = ifc_meshes.get(str(oid))
|
||||||
if mesh is not None:
|
if mesh is not None:
|
||||||
result.append(mesh)
|
result.append(mesh)
|
||||||
return result
|
return result, []
|
||||||
|
|
||||||
|
|
||||||
def _resolve_instance_scale(obj, stream_scale: float) -> float:
|
def _resolve_instance_scale(obj, stream_scale: float) -> float:
|
||||||
@@ -183,7 +214,7 @@ def _resolve_instance_scale(obj, stream_scale: float) -> float:
|
|||||||
try:
|
try:
|
||||||
units = obj[key]
|
units = obj[key]
|
||||||
if units and isinstance(units, str):
|
if units and isinstance(units, str):
|
||||||
s = _UNIT_SCALES.get(units.lower().strip())
|
s = MM_SCALES.get(units.lower().strip())
|
||||||
if s is not None:
|
if s is not None:
|
||||||
return s
|
return s
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -206,20 +237,13 @@ _rep_map_cache: dict = {}
|
|||||||
_identity_placement_cache: dict[int, object] = {}
|
_identity_placement_cache: dict[int, object] = {}
|
||||||
|
|
||||||
|
|
||||||
_MM_SCALES = {
|
|
||||||
"mm": 1.0, "millimeter": 1.0, "millimeters": 1.0,
|
|
||||||
"cm": 10.0, "centimeter": 10.0,
|
|
||||||
"m": 1000.0, "meter": 1000.0, "meters": 1000.0,
|
|
||||||
"ft": 304.8, "in": 25.4,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# IfcRepresentationMap builder — geometry created once per definition
|
# IfcRepresentationMap builder — geometry created once per definition
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def _build_rep_map(ifc, body_context, meshes: list, ifc_format: bool,
|
def _build_rep_map(ifc, body_context, meshes: list, ifc_format: bool,
|
||||||
material_manager=None):
|
material_manager=None, fallback_app_ids: list = None,
|
||||||
|
definition_id: str = None):
|
||||||
"""
|
"""
|
||||||
Build an IfcRepresentationMap from definition meshes.
|
Build an IfcRepresentationMap from definition meshes.
|
||||||
Geometry is in local coordinates (mm, no instance transform applied).
|
Geometry is in local coordinates (mm, no instance transform applied).
|
||||||
@@ -234,18 +258,18 @@ def _build_rep_map(ifc, body_context, meshes: list, ifc_format: bool,
|
|||||||
else:
|
else:
|
||||||
raw_verts = _get(mesh, "vertices") or []
|
raw_verts = _get(mesh, "vertices") or []
|
||||||
raw_faces = _get(mesh, "faces") or []
|
raw_faces = _get(mesh, "faces") or []
|
||||||
verts = unwrap_chunks(list(raw_verts))
|
verts = unwrap_chunks(raw_verts if isinstance(raw_verts, list) else list(raw_verts))
|
||||||
faces_raw = unwrap_chunks(list(raw_faces))
|
faces_raw = unwrap_chunks(raw_faces if isinstance(raw_faces, list) else list(raw_faces))
|
||||||
if not verts or not faces_raw:
|
if not verts or not faces_raw:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mesh_units = _get(mesh, "units") or _get(mesh, "_units") or ("m" if ifc_format else "mm")
|
mesh_units = _get(mesh, "units") or _get(mesh, "_units") or ("m" if ifc_format else "mm")
|
||||||
ms = _MM_SCALES.get(mesh_units.lower().strip(), 1.0)
|
ms = MM_SCALES.get(mesh_units.lower().strip(), 1.0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
face_groups = decode_faces(faces_raw)
|
face_groups = decode_faces(faces_raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ Instance face decode: {e}")
|
print(f" Warning: Instance face decode: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Scale vertices once and cache the result
|
# Scale vertices once and cache the result
|
||||||
@@ -260,11 +284,29 @@ def _build_rep_map(ifc, body_context, meshes: list, ifc_format: bool,
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Apply material style to each faceset
|
# Apply material style to each faceset
|
||||||
|
# Try: mesh applicationId → fallback IDs → definitionId mapping
|
||||||
if material_manager:
|
if material_manager:
|
||||||
mesh_app_id = _get(mesh, "applicationId")
|
mesh_app_id = _get(mesh, "applicationId")
|
||||||
|
style = None
|
||||||
if mesh_app_id:
|
if mesh_app_id:
|
||||||
|
style = material_manager.get_style(str(mesh_app_id))
|
||||||
|
if not style and fallback_app_ids:
|
||||||
|
for fid in fallback_app_ids:
|
||||||
|
style = material_manager.get_style(fid)
|
||||||
|
if style:
|
||||||
|
break
|
||||||
|
if not style and definition_id:
|
||||||
|
style = material_manager.get_style_by_definition(definition_id)
|
||||||
|
if style:
|
||||||
for fs in mesh_facesets:
|
for fs in mesh_facesets:
|
||||||
material_manager.apply_to_item(fs, str(mesh_app_id))
|
try:
|
||||||
|
ifcopenshell.api.run(
|
||||||
|
"style.assign_item_style", ifc,
|
||||||
|
item=fs, style=style,
|
||||||
|
)
|
||||||
|
material_manager._apply_count += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
geom_items.extend(mesh_facesets)
|
geom_items.extend(mesh_facesets)
|
||||||
|
|
||||||
@@ -387,9 +429,9 @@ def instance_to_ifc(ifc, body_context, obj: Base, definition_map: dict,
|
|||||||
# --- Get or build IfcRepresentationMap (cached per definition_id) ---
|
# --- Get or build IfcRepresentationMap (cached per definition_id) ---
|
||||||
if definition_id not in _rep_map_cache:
|
if definition_id not in _rep_map_cache:
|
||||||
if ifc_format:
|
if ifc_format:
|
||||||
meshes = _get_ifc_meshes(definition_id, definition_map)
|
meshes, extra_app_ids = _get_ifc_meshes(definition_id, definition_map)
|
||||||
else:
|
else:
|
||||||
meshes = _get_revit_meshes(definition_id, definition_map)
|
meshes, extra_app_ids = _get_revit_meshes(definition_id, definition_map)
|
||||||
|
|
||||||
if not meshes:
|
if not meshes:
|
||||||
_stats["not_found"] += 1
|
_stats["not_found"] += 1
|
||||||
@@ -397,8 +439,17 @@ def instance_to_ifc(ifc, body_context, obj: Base, definition_map: dict,
|
|||||||
return None, placement
|
return None, placement
|
||||||
|
|
||||||
_stats["found"] += 1
|
_stats["found"] += 1
|
||||||
|
# Build fallback app_id list: instance's own + definition chain IDs
|
||||||
|
instance_app_id = _get(obj, "applicationId")
|
||||||
|
fallback_ids = []
|
||||||
|
if instance_app_id:
|
||||||
|
fallback_ids.append(str(instance_app_id))
|
||||||
|
fallback_ids.extend(extra_app_ids)
|
||||||
|
|
||||||
_rep_map_cache[definition_id] = _build_rep_map(
|
_rep_map_cache[definition_id] = _build_rep_map(
|
||||||
ifc, body_context, meshes, ifc_format, material_manager
|
ifc, body_context, meshes, ifc_format, material_manager,
|
||||||
|
fallback_app_ids=fallback_ids,
|
||||||
|
definition_id=definition_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Track stats even for cached definitions
|
# Track stats even for cached definitions
|
||||||
@@ -454,11 +505,19 @@ def get_definition_object(obj: Base, definition_map: dict):
|
|||||||
return source
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
def is_definition_source(obj, definition_map: dict) -> bool:
|
||||||
|
"""Return True if this object is a definition geometry source (should not be exported standalone)."""
|
||||||
|
app_id = _get(obj, "applicationId")
|
||||||
|
if not app_id:
|
||||||
|
return False
|
||||||
|
return str(app_id).lower() in definition_map.get("definition_sources", set())
|
||||||
|
|
||||||
|
|
||||||
def print_instance_stats():
|
def print_instance_stats():
|
||||||
total = _stats["found"] + _stats["not_found"]
|
total = _stats["found"] + _stats["not_found"]
|
||||||
print(f" Instance resolution: {_stats['found']}/{total} definitions found")
|
print(f" Instance resolution: {_stats['found']}/{total} definitions found")
|
||||||
if _stats["not_found"] > 0:
|
if _stats["not_found"] > 0:
|
||||||
print(f" ⚠️ {_stats['not_found']} instances had no definition geometry")
|
print(f" Warning: {_stats['not_found']} instances had no definition geometry")
|
||||||
|
|
||||||
|
|
||||||
def reset_caches():
|
def reset_caches():
|
||||||
|
|||||||
+41
-17
@@ -25,6 +25,7 @@
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
from utils.helpers import _get
|
||||||
|
|
||||||
|
|
||||||
def _argb_to_rgb(argb_int: int) -> tuple[float, float, float]:
|
def _argb_to_rgb(argb_int: int) -> tuple[float, float, float]:
|
||||||
@@ -36,22 +37,6 @@ def _argb_to_rgb(argb_int: int) -> tuple[float, float, float]:
|
|||||||
return r, g, b
|
return r, g, b
|
||||||
|
|
||||||
|
|
||||||
def _get(obj, key, default=None):
|
|
||||||
try:
|
|
||||||
val = getattr(obj, key, None)
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
val = obj[key]
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
class MaterialManager:
|
class MaterialManager:
|
||||||
"""
|
"""
|
||||||
Builds a lookup from mesh applicationId → IfcSurfaceStyle,
|
Builds a lookup from mesh applicationId → IfcSurfaceStyle,
|
||||||
@@ -64,6 +49,8 @@ class MaterialManager:
|
|||||||
self._style_map: dict[str, object] = {}
|
self._style_map: dict[str, object] = {}
|
||||||
# name → IfcSurfaceStyle (cache to avoid duplicates)
|
# name → IfcSurfaceStyle (cache to avoid duplicates)
|
||||||
self._style_cache: dict[str, object] = {}
|
self._style_cache: dict[str, object] = {}
|
||||||
|
self._apply_count = 0
|
||||||
|
self._miss_count = 0
|
||||||
self._build(root)
|
self._build(root)
|
||||||
|
|
||||||
def _build(self, root: Base):
|
def _build(self, root: Base):
|
||||||
@@ -139,6 +126,7 @@ class MaterialManager:
|
|||||||
"""Assign the material style to a single IFC geometry item (e.g. IfcPolygonalFaceSet)."""
|
"""Assign the material style to a single IFC geometry item (e.g. IfcPolygonalFaceSet)."""
|
||||||
style = self.get_style(mesh_app_id)
|
style = self.get_style(mesh_app_id)
|
||||||
if style is None:
|
if style is None:
|
||||||
|
self._miss_count += 1
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
ifcopenshell.api.run(
|
ifcopenshell.api.run(
|
||||||
@@ -147,5 +135,41 @@ class MaterialManager:
|
|||||||
item=item,
|
item=item,
|
||||||
style=style,
|
style=style,
|
||||||
)
|
)
|
||||||
|
self._apply_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass # Non-fatal — geometry still exports without colour
|
pass # Non-fatal — geometry still exports without colour
|
||||||
|
|
||||||
|
def build_definition_material_map(self, definition_map: dict):
|
||||||
|
"""
|
||||||
|
Build a mapping from definitionId → material data by resolving which
|
||||||
|
InstanceProxy objects the material proxy references and finding their definitionId.
|
||||||
|
This handles the case where renderMaterialProxies.objects references inner
|
||||||
|
InstanceProxy applicationIds rather than the top-level element applicationIds.
|
||||||
|
"""
|
||||||
|
by_app_id = definition_map.get("by_app_id", {})
|
||||||
|
self._definition_material: dict[str, tuple] = {} # definitionId → (name, diffuse, transparency)
|
||||||
|
|
||||||
|
for app_id_key, mat_data in self._material_data.items():
|
||||||
|
obj = by_app_id.get(app_id_key)
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
def_id = _get(obj, "definitionId")
|
||||||
|
if def_id and isinstance(def_id, str):
|
||||||
|
self._definition_material[def_id.lower()] = mat_data
|
||||||
|
|
||||||
|
if self._definition_material:
|
||||||
|
print(f" Material definitionId mappings: {len(self._definition_material)}")
|
||||||
|
|
||||||
|
def get_style_by_definition(self, definition_id: str):
|
||||||
|
"""Return IfcSurfaceStyle for a definitionId (created on demand), or None."""
|
||||||
|
if not hasattr(self, '_definition_material'):
|
||||||
|
return None
|
||||||
|
key = str(definition_id).lower()
|
||||||
|
data = self._definition_material.get(key)
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
name, diffuse, transparency = data
|
||||||
|
return self._get_or_create_style(name, diffuse, transparency)
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
print(f" Materials applied: {self._apply_count}, missed: {self._miss_count}")
|
||||||
+28
-23
@@ -14,33 +14,13 @@
|
|||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api
|
import ifcopenshell.api
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
from utils.helpers import _get
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Safe access helpers
|
# Safe access helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _get(obj, key, default=None):
|
|
||||||
"""Safe access for both dicts and Speckle Base objects."""
|
|
||||||
if obj is None:
|
|
||||||
return default
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
return obj.get(key, default)
|
|
||||||
try:
|
|
||||||
val = getattr(obj, key, None)
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
val = obj[key]
|
|
||||||
if val is not None:
|
|
||||||
return val
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def _to_dict(obj) -> dict:
|
def _to_dict(obj) -> dict:
|
||||||
"""Convert a Speckle Base object or dict to a plain dict."""
|
"""Convert a Speckle Base object or dict to a plain dict."""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
@@ -150,6 +130,20 @@ _UNIT_QTY_MAP = {
|
|||||||
"degree": ("IfcQuantityCount", "CountValue"),
|
"degree": ("IfcQuantityCount", "CountValue"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Name keyword → IFC quantity type (used when no units are provided)
|
||||||
|
_NAME_QTY_MAP = {
|
||||||
|
"length": ("IfcQuantityLength", "LengthValue"),
|
||||||
|
"width": ("IfcQuantityLength", "LengthValue"),
|
||||||
|
"height": ("IfcQuantityLength", "LengthValue"),
|
||||||
|
"depth": ("IfcQuantityLength", "LengthValue"),
|
||||||
|
"perimeter": ("IfcQuantityLength", "LengthValue"),
|
||||||
|
"area": ("IfcQuantityArea", "AreaValue"),
|
||||||
|
"volume": ("IfcQuantityVolume", "VolumeValue"),
|
||||||
|
"volumn": ("IfcQuantityVolume", "VolumeValue"), # common typo
|
||||||
|
"weight": ("IfcQuantityWeight", "WeightValue"),
|
||||||
|
"mass": ("IfcQuantityWeight", "WeightValue"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Set IFC element attributes from _properties.Attributes
|
# Set IFC element attributes from _properties.Attributes
|
||||||
@@ -240,7 +234,10 @@ def _try_float(value):
|
|||||||
def write_quantity_sets(ifc, element, obj):
|
def write_quantity_sets(ifc, element, obj):
|
||||||
"""Write all quantity sets from _properties.Quantities."""
|
"""Write all quantity sets from _properties.Quantities."""
|
||||||
props = get_properties(obj)
|
props = get_properties(obj)
|
||||||
quantities_section = _to_dict(props.get("Quantities"))
|
quantities_raw = props.get("Quantities")
|
||||||
|
if quantities_raw is None:
|
||||||
|
return
|
||||||
|
quantities_section = _to_dict(quantities_raw)
|
||||||
if not quantities_section:
|
if not quantities_section:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -273,14 +270,22 @@ def write_quantity_sets(ifc, element, obj):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
mapping = _UNIT_QTY_MAP.get(units)
|
mapping = _UNIT_QTY_MAP.get(units)
|
||||||
|
if not mapping:
|
||||||
|
# Infer quantity type from name keywords
|
||||||
|
name_lower = name.lower()
|
||||||
|
for keyword, m in _NAME_QTY_MAP.items():
|
||||||
|
if keyword in name_lower:
|
||||||
|
mapping = m
|
||||||
|
break
|
||||||
if mapping:
|
if mapping:
|
||||||
qty_type, value_attr = mapping
|
qty_type, value_attr = mapping
|
||||||
qty = ifc.create_entity(
|
qty = ifc.create_entity(
|
||||||
qty_type, Name=name, **{value_attr: value}
|
qty_type, Name=name, **{value_attr: value}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# CountValue requires int
|
||||||
qty = ifc.create_entity(
|
qty = ifc.create_entity(
|
||||||
"IfcQuantityCount", Name=name, CountValue=value
|
"IfcQuantityCount", Name=name, CountValue=int(value)
|
||||||
)
|
)
|
||||||
quantities.append(qty)
|
quantities.append(qty)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ class StoreyManager:
|
|||||||
products=[storey],
|
products=[storey],
|
||||||
)
|
)
|
||||||
self._storeys[level_name] = storey
|
self._storeys[level_name] = storey
|
||||||
print(f" 🏢 Created storey: {level_name}")
|
print(f" Created storey: {level_name}")
|
||||||
|
|
||||||
return self._storeys[level_name]
|
return self._storeys[level_name]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user