Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11acb02fd1 | |||
| f7aa6c29da | |||
| 63082a881c | |||
| bdd030ba86 |
@@ -30,3 +30,4 @@ jobs:
|
||||
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
|
||||
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
|
||||
speckle_function_command: "python -u main.py run"
|
||||
speckle_function_recommended_memory_mi: 5000
|
||||
|
||||
@@ -6,15 +6,16 @@ import utils.config as config
|
||||
|
||||
from utils.materials import MaterialManager
|
||||
from utils.traversal import traverse, print_tree
|
||||
from utils.mapper import classify
|
||||
from utils.mapper import classify, get_predefined_type, reset_caches as reset_mapper_caches
|
||||
from utils.geometry import mesh_to_ifc, get_display_instances, _make_placement
|
||||
from utils.instances import is_instance, instance_to_ifc, build_definition_map, print_instance_stats
|
||||
from utils.properties import write_properties, write_common_properties
|
||||
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
|
||||
from utils.type_manager import TypeManager
|
||||
|
||||
|
||||
SPATIAL_STRUCTURE_TYPES = {
|
||||
"IfcSite", "IfcBuilding", "IfcBuildingStorey",
|
||||
"IfcBuilding", "IfcBuildingStorey",
|
||||
"IfcSpace", "IfcExternalSpatialElement", "IfcSpatialZone",
|
||||
"IfcGrid", "IfcAnnotation",
|
||||
}
|
||||
@@ -59,7 +60,9 @@ def automate_function(
|
||||
print(" Speckle -> IFC4.3 Exporter")
|
||||
print("=" * 60)
|
||||
|
||||
#version_root_object = automate_context.receive_version()
|
||||
# Reset caches from any previous run
|
||||
reset_props_caches()
|
||||
reset_mapper_caches()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Receive
|
||||
@@ -79,7 +82,7 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. Set up IFC
|
||||
# ------------------------------------------------------------------ #
|
||||
ifc, building, body_context = create_ifc_scaffold()
|
||||
ifc, _site, building, body_context = create_ifc_scaffold()
|
||||
storey_manager = StoreyManager(ifc, building)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -87,6 +90,7 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
print("\n🎨 Building material map...")
|
||||
material_manager = MaterialManager(ifc, base)
|
||||
type_manager = TypeManager(ifc)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Traverse & export
|
||||
@@ -106,21 +110,31 @@ def automate_function(
|
||||
skipped_spatial += 1
|
||||
continue
|
||||
|
||||
name = getattr(obj, "name", None) or getattr(obj, "applicationId", None) or getattr(obj, "id", "unnamed")
|
||||
name = build_element_name(obj)
|
||||
storey = storey_manager.get_or_create(level_name)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Path A: Instance object (has transform + definitionId, no displayValue)
|
||||
# ------------------------------------------------------------------ #
|
||||
if is_instance(obj):
|
||||
# Instances may lack category info — inherit from definition object
|
||||
if ifc_class == "IfcBuildingElementProxy":
|
||||
def_obj = get_definition_object(obj, definition_map)
|
||||
if def_obj:
|
||||
ifc_class = classify(def_obj, category_name)
|
||||
|
||||
rep, placement = instance_to_ifc(ifc, body_context, obj, definition_map, scale=scale, material_manager=material_manager)
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey)
|
||||
write_common_properties(ifc, element, obj, category_name)
|
||||
write_properties(ifc, element, obj)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
if not rep:
|
||||
no_geometry += 1
|
||||
continue
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj),
|
||||
object_type=getattr(obj, "type", None),
|
||||
predefined_type=get_predefined_type(obj))
|
||||
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
|
||||
|
||||
else:
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -131,12 +145,14 @@ def automate_function(
|
||||
|
||||
# B1: Mesh geometry on the parent object
|
||||
rep, placement = mesh_to_ifc(ifc, body_context, obj, scale=scale, material_manager=material_manager)
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey)
|
||||
write_common_properties(ifc, element, obj, category_name)
|
||||
write_properties(ifc, element, obj)
|
||||
total += 1
|
||||
if not rep:
|
||||
no_geometry += 1
|
||||
if rep:
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj),
|
||||
object_type=getattr(obj, "type", None),
|
||||
predefined_type=get_predefined_type(obj))
|
||||
write_properties(ifc, element, obj, ifc_class=ifc_class, category_name=category_name)
|
||||
type_manager.assign(element, obj, ifc_class)
|
||||
total += 1
|
||||
|
||||
# B2: Instance objects nested inside displayValue
|
||||
# Each becomes its own IFC element (same class as parent)
|
||||
@@ -146,15 +162,23 @@ def automate_function(
|
||||
inst_rep, inst_placement = instance_to_ifc(
|
||||
ifc, body_context, inst, definition_map, scale=scale, material_manager=material_manager
|
||||
)
|
||||
inst_element = _create_element(
|
||||
ifc, ifc_class, name, inst_rep, inst_placement, storey
|
||||
)
|
||||
write_common_properties(ifc, inst_element, obj, category_name)
|
||||
write_properties(ifc, inst_element, obj)
|
||||
instance_count += 1
|
||||
total += 1
|
||||
if not inst_rep:
|
||||
no_geometry += 1
|
||||
continue
|
||||
inst_element = _create_element(
|
||||
ifc, ifc_class, name, inst_rep, inst_placement, storey,
|
||||
tag=get_element_tag(obj), guid=None,
|
||||
object_type=getattr(obj, "type", None),
|
||||
predefined_type=get_predefined_type(obj),
|
||||
)
|
||||
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
|
||||
|
||||
# Track if neither path produced geometry
|
||||
if not rep and not nested_instances:
|
||||
no_geometry += 1
|
||||
|
||||
if total % 100 == 0:
|
||||
print(f" ... processed {total} elements")
|
||||
@@ -162,13 +186,22 @@ def automate_function(
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Write output
|
||||
# ------------------------------------------------------------------ #
|
||||
print("\n🔗 Flushing type relationships...")
|
||||
type_manager.flush()
|
||||
|
||||
file_name = function_inputs.file_name
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
ifc_filename = f"{file_name}_{timestamp}.ifc"
|
||||
|
||||
ifc.write(ifc_filename)
|
||||
automate_context.store_file_result(f"./{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}")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Export complete!")
|
||||
@@ -181,13 +214,28 @@ def automate_function(
|
||||
print_instance_stats()
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def _create_element(ifc, ifc_class, name, rep, placement, storey):
|
||||
def _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=None, guid=None, object_type=None, predefined_type=None):
|
||||
"""Helper: create an IFC element, assign geometry + placement + container."""
|
||||
element = ifcopenshell.api.run(
|
||||
"root.create_entity", ifc,
|
||||
ifc_class=ifc_class,
|
||||
name=str(name),
|
||||
)
|
||||
kwargs = {"ifc_class": ifc_class, "name": str(name)}
|
||||
if predefined_type:
|
||||
kwargs["predefined_type"] = predefined_type
|
||||
element = ifcopenshell.api.run("root.create_entity", ifc, **kwargs)
|
||||
if tag:
|
||||
try:
|
||||
element.Tag = str(tag)
|
||||
except AttributeError:
|
||||
pass
|
||||
if object_type:
|
||||
try:
|
||||
element.ObjectType = str(object_type)
|
||||
except AttributeError:
|
||||
pass
|
||||
if guid:
|
||||
try:
|
||||
element.GlobalId = guid
|
||||
except Exception:
|
||||
pass
|
||||
if rep and placement:
|
||||
element.Representation = ifc.createIfcProductDefinitionShape(
|
||||
Representations=(rep,)
|
||||
@@ -198,11 +246,20 @@ def _create_element(ifc, ifc_class, name, rep, placement, storey):
|
||||
else:
|
||||
element.ObjectPlacement = _make_placement(ifc, 0.0, 0.0, 0.0)
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"spatial.assign_container", ifc,
|
||||
relating_structure=storey,
|
||||
products=[element],
|
||||
)
|
||||
# IfcSite is a spatial structure element — can't use spatial.assign_container.
|
||||
# Use aggregate.assign_object to nest it under the storey instead.
|
||||
if ifc_class == "IfcSite":
|
||||
ifcopenshell.api.run(
|
||||
"aggregate.assign_object", ifc,
|
||||
relating_object=storey,
|
||||
products=[element],
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.run(
|
||||
"spatial.assign_container", ifc,
|
||||
relating_structure=storey,
|
||||
products=[element],
|
||||
)
|
||||
return element
|
||||
|
||||
# make sure to call the function with the executor
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+90
-137
@@ -1,12 +1,14 @@
|
||||
# =============================================================================
|
||||
# geometry.py
|
||||
# Converts Speckle DataObject geometry → IFC IfcFacetedBrep + IfcLocalPlacement
|
||||
# Converts Speckle DataObject geometry → IFC IfcPolygonalFaceSet + IfcLocalPlacement
|
||||
#
|
||||
# Key facts:
|
||||
# - After specklepy receive(), vertices and faces are FLAT Python lists
|
||||
# - displayValue is an array of Mesh objects
|
||||
# - Units are in mm (for Revit), scale to metres for IFC
|
||||
# - Vertices are in absolute world coordinates
|
||||
# - Uses IfcPolygonalFaceSet (indexed vertices) instead of IfcFacetedBrep
|
||||
# for compact output — each vertex stored once, not once per face.
|
||||
# =============================================================================
|
||||
|
||||
import ifcopenshell
|
||||
@@ -24,7 +26,7 @@ _UNIT_SCALES = {
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Geometry validation helpers (GEM111 + BRP002 fixes)
|
||||
# Geometry validation helpers (GEM111 fix)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# Minimum distance in mm below which two vertices are considered identical (GEM111).
|
||||
@@ -36,127 +38,75 @@ def snap_coord(v: float) -> int:
|
||||
return round(v / _VERTEX_MERGE_TOL)
|
||||
|
||||
|
||||
def _find_connected_components(snapped_faces: list) -> list:
|
||||
def build_ifc_facesets(ifc, verts_scaled: list, face_groups: list) -> list:
|
||||
"""
|
||||
Union-Find: group face indices into connected components.
|
||||
Two faces are connected if they share an edge (pair of snapped vertex keys).
|
||||
Returns list of components, each a list of face indices.
|
||||
Build a list of IfcPolygonalFaceSet from scaled (x,y,z) vertices and face index groups.
|
||||
|
||||
BRP002 requires all faces in an IfcClosedShell to form ONE component.
|
||||
If multiple components exist, each must become a separate IfcClosedShell.
|
||||
"""
|
||||
n = len(snapped_faces)
|
||||
if n == 0:
|
||||
return []
|
||||
|
||||
parent = list(range(n))
|
||||
|
||||
def find(x):
|
||||
while parent[x] != x:
|
||||
parent[x] = parent[parent[x]]
|
||||
x = parent[x]
|
||||
return x
|
||||
|
||||
def union(a, b):
|
||||
parent[find(a)] = find(b)
|
||||
|
||||
# Map each edge to the first face that used it, then union subsequent faces
|
||||
edge_to_face = {}
|
||||
for fi, keys in enumerate(snapped_faces):
|
||||
for i in range(len(keys)):
|
||||
edge = frozenset([keys[i], keys[(i + 1) % len(keys)]])
|
||||
if edge in edge_to_face:
|
||||
union(fi, edge_to_face[edge])
|
||||
else:
|
||||
edge_to_face[edge] = fi
|
||||
|
||||
from collections import defaultdict
|
||||
groups: dict = defaultdict(list)
|
||||
for fi in range(n):
|
||||
groups[find(fi)].append(fi)
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def build_ifc_breps(ifc, verts_scaled: list, face_groups: list) -> list:
|
||||
"""
|
||||
Build a list of IfcFacetedBrep from scaled (x,y,z) vertices and face index groups.
|
||||
Uses IfcCartesianPointList3D + IfcIndexedPolygonalFace for compact output.
|
||||
Vertices are deduplicated via snap grid so each unique position is stored once.
|
||||
|
||||
GEM111 fix: skip faces with near-duplicate vertices (snapped to same grid cell).
|
||||
BRP002 fix: split faces into connected components; each component → its own
|
||||
IfcClosedShell → IfcFacetedBrep so every shell is arc-wise connected.
|
||||
|
||||
verts_scaled: flat list of already-scaled floats [x0,y0,z0, x1,y1,z1, ...]
|
||||
face_groups: list of index lists [[i,j,k], [i,j,k,l], ...]
|
||||
Returns: list of IfcFacetedBrep (one per connected component, never empty).
|
||||
Returns: list of IfcPolygonalFaceSet (typically one, empty on failure).
|
||||
"""
|
||||
# Pass 1: validate faces and build snapped key lists for connectivity analysis
|
||||
valid_faces = [] # list of (pts_raw, snapped_keys)
|
||||
# Build deduplicated vertex list via snap grid
|
||||
snap_to_idx = {} # snap_key → 0-based index in deduped_verts
|
||||
deduped_verts = [] # [(x, y, z), ...]
|
||||
|
||||
def get_vertex_index(x, y, z):
|
||||
key = (snap_coord(x), snap_coord(y), snap_coord(z))
|
||||
if key in snap_to_idx:
|
||||
return snap_to_idx[key], key
|
||||
idx = len(deduped_verts)
|
||||
snap_to_idx[key] = idx
|
||||
deduped_verts.append((x, y, z))
|
||||
return idx, key
|
||||
|
||||
# Validate faces and remap indices to deduplicated vertex list
|
||||
valid_faces = [] # list of [idx0, idx1, idx2, ...] (0-based into deduped_verts)
|
||||
for indices in face_groups:
|
||||
try:
|
||||
pts_raw = []
|
||||
snapped = []
|
||||
remapped = []
|
||||
seen_snaps = set()
|
||||
degenerate = False
|
||||
seen = set()
|
||||
|
||||
for i in indices:
|
||||
x = float(verts_scaled[i * 3])
|
||||
y = float(verts_scaled[i * 3 + 1])
|
||||
z = float(verts_scaled[i * 3 + 2])
|
||||
key = (snap_coord(x), snap_coord(y), snap_coord(z))
|
||||
if key in seen:
|
||||
idx, snap_key = get_vertex_index(x, y, z)
|
||||
if snap_key in seen_snaps:
|
||||
degenerate = True
|
||||
break
|
||||
seen.add(key)
|
||||
pts_raw.append((x, y, z))
|
||||
snapped.append(key)
|
||||
seen_snaps.add(snap_key)
|
||||
remapped.append(idx)
|
||||
|
||||
if degenerate or len(pts_raw) < 3:
|
||||
if degenerate or len(remapped) < 3:
|
||||
continue
|
||||
|
||||
valid_faces.append((pts_raw, snapped))
|
||||
valid_faces.append(remapped)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not valid_faces:
|
||||
if not valid_faces or not deduped_verts:
|
||||
return []
|
||||
|
||||
# Pass 2: split into connected components (BRP002)
|
||||
snapped_only = [f[1] for f in valid_faces]
|
||||
components = _find_connected_components(snapped_only)
|
||||
|
||||
# Pass 3: build one IfcFacetedBrep per component
|
||||
breps = []
|
||||
for component_indices in components:
|
||||
# Build IFC entities
|
||||
try:
|
||||
point_list = ifc.createIfcCartesianPointList3D(
|
||||
[list(v) for v in deduped_verts]
|
||||
)
|
||||
ifc_faces = []
|
||||
for fi in component_indices:
|
||||
pts_raw, _ = valid_faces[fi]
|
||||
try:
|
||||
pts = [ifc.createIfcCartesianPoint([x, y, z]) for x, y, z in pts_raw]
|
||||
poly = ifc.createIfcPolyLoop(pts)
|
||||
bound = ifc.createIfcFaceOuterBound(poly, True)
|
||||
ifc_faces.append(ifc.createIfcFace([bound]))
|
||||
except Exception:
|
||||
continue
|
||||
for face_indices in valid_faces:
|
||||
# IfcIndexedPolygonalFace uses 1-based indices
|
||||
coord_index = [idx + 1 for idx in face_indices]
|
||||
ifc_faces.append(ifc.createIfcIndexedPolygonalFace(coord_index))
|
||||
|
||||
if not ifc_faces:
|
||||
continue
|
||||
|
||||
shell = ifc.createIfcClosedShell(ifc_faces)
|
||||
breps.append(ifc.createIfcFacetedBrep(shell))
|
||||
|
||||
return breps
|
||||
|
||||
|
||||
# Keep old name as alias so instances.py import works unchanged
|
||||
def build_ifc_faces(ifc, verts_scaled: list, face_groups: list) -> list:
|
||||
"""Legacy wrapper — returns flat list of IfcFace (no connectivity splitting)."""
|
||||
# Used only as a fallback; callers should prefer build_ifc_breps directly.
|
||||
breps = build_ifc_breps(ifc, verts_scaled, face_groups)
|
||||
# Return the faces from all shells combined (for callers that need face lists)
|
||||
faces = []
|
||||
for brep in breps:
|
||||
faces.extend(brep.Outer.CfsFaces)
|
||||
return faces
|
||||
faceset = ifc.createIfcPolygonalFaceSet(point_list, None, ifc_faces, None)
|
||||
return [faceset]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -189,31 +139,32 @@ def unwrap_chunks(raw) -> list:
|
||||
|
||||
Handles two cases:
|
||||
1. Already flat list of numbers (after specklepy receive deserializes)
|
||||
→ [3, 0, 1, 2, 3, ...] returned as-is
|
||||
→ returned as-is (fast path)
|
||||
2. List of DataChunk objects (raw from server before deserialization)
|
||||
→ each chunk's .data list is concatenated
|
||||
|
||||
Both cases are handled so this function is always safe to call.
|
||||
"""
|
||||
if not raw:
|
||||
return []
|
||||
|
||||
# Fast path: if first item is a number, assume all items are numbers
|
||||
first = raw[0]
|
||||
if isinstance(first, (int, float)):
|
||||
return raw
|
||||
|
||||
# Slow path: DataChunk objects or mixed content
|
||||
result = []
|
||||
for item in raw:
|
||||
if item is None:
|
||||
continue
|
||||
# Plain number — already flat
|
||||
if isinstance(item, (int, float)):
|
||||
result.append(item)
|
||||
continue
|
||||
# DataChunk — unwrap .data
|
||||
speckle_type = getattr(item, "speckle_type", "") or ""
|
||||
if "DataChunk" in speckle_type:
|
||||
chunk_data = _get(item, "data") or _get(item, "@data")
|
||||
if chunk_data:
|
||||
result.extend(list(chunk_data))
|
||||
else:
|
||||
# Unknown — try iterating (handles nested lists)
|
||||
try:
|
||||
result.extend(list(item))
|
||||
except Exception:
|
||||
@@ -318,17 +269,18 @@ def decode_faces(faces_raw: list) -> list:
|
||||
"""
|
||||
decoded = []
|
||||
i = 0
|
||||
while i < len(faces_raw):
|
||||
total = len(faces_raw)
|
||||
while i < total:
|
||||
n = int(faces_raw[i])
|
||||
if n == 0:
|
||||
n = 3
|
||||
elif n == 1:
|
||||
n = 4
|
||||
end = i + 1 + n
|
||||
if end > len(faces_raw):
|
||||
if end > total:
|
||||
break
|
||||
indices = [int(faces_raw[i + 1 + j]) for j in range(n)]
|
||||
decoded.append(indices)
|
||||
# Direct slice is faster than list comprehension with int()
|
||||
decoded.append([int(v) for v in faces_raw[i + 1:end]])
|
||||
i = end
|
||||
return decoded
|
||||
|
||||
@@ -374,7 +326,7 @@ def mesh_to_ifc(
|
||||
) -> tuple:
|
||||
"""
|
||||
Convert a Speckle DataObject → (IfcShapeRepresentation, IfcLocalPlacement).
|
||||
Creates one IfcFacetedBrep per mesh so each can carry its own material style.
|
||||
Creates one IfcPolygonalFaceSet per mesh so each can carry its own material style.
|
||||
Returns (None, None) if no usable geometry is found.
|
||||
"""
|
||||
meshes = get_display_meshes(obj)
|
||||
@@ -384,21 +336,22 @@ def mesh_to_ifc(
|
||||
obj_scale = _resolve_scale(obj, scale)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Pass 1: collect all scaled vertices to compute world origin
|
||||
# Pass 1: unpack vertices once per mesh, collect all scaled coords
|
||||
# to compute world origin. Cache (verts, ms) for Pass 2.
|
||||
# ------------------------------------------------------------------ #
|
||||
mesh_cache = [] # [(verts_list, ms, scaled)] or None per mesh
|
||||
all_scaled = []
|
||||
for mesh in meshes:
|
||||
raw_verts = _get(mesh, "vertices") or []
|
||||
verts = unwrap_chunks(list(raw_verts))
|
||||
if not verts:
|
||||
mesh_cache.append(None)
|
||||
continue
|
||||
ms = _resolve_scale(mesh, obj_scale)
|
||||
for i in range(0, len(verts) - 2, 3):
|
||||
all_scaled.extend([
|
||||
float(verts[i]) * ms,
|
||||
float(verts[i+1]) * ms,
|
||||
float(verts[i+2]) * ms,
|
||||
])
|
||||
# Pre-scale vertices once, reuse in Pass 2
|
||||
scaled = [float(v) * ms for v in verts]
|
||||
mesh_cache.append((verts, ms, scaled))
|
||||
all_scaled.extend(scaled)
|
||||
|
||||
if not all_scaled:
|
||||
return None, None
|
||||
@@ -406,49 +359,49 @@ def mesh_to_ifc(
|
||||
ox, oy, oz = compute_origin(all_scaled)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Pass 2: one brep per mesh (so each can have its own material style)
|
||||
# Pass 2: one faceset per mesh — reuse cached verts, only unpack faces
|
||||
# ------------------------------------------------------------------ #
|
||||
brep_items = []
|
||||
geom_items = []
|
||||
|
||||
for mesh in meshes:
|
||||
raw_verts = _get(mesh, "vertices") or []
|
||||
for mesh, cached in zip(meshes, mesh_cache):
|
||||
if cached is None:
|
||||
continue
|
||||
verts, ms, scaled = cached
|
||||
raw_faces = _get(mesh, "faces") or []
|
||||
verts = unwrap_chunks(list(raw_verts))
|
||||
faces_raw = unwrap_chunks(list(raw_faces))
|
||||
|
||||
if not verts or not faces_raw:
|
||||
if not faces_raw:
|
||||
continue
|
||||
|
||||
ms = _resolve_scale(mesh, obj_scale)
|
||||
|
||||
try:
|
||||
face_groups = decode_faces(faces_raw)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Face decode error: {e}")
|
||||
continue
|
||||
|
||||
# Build pre-scaled vertex list (relative to origin) for this mesh
|
||||
verts_scaled = []
|
||||
for vi in range(0, len(verts) - 2, 3):
|
||||
verts_scaled.append(float(verts[vi]) * ms - ox)
|
||||
verts_scaled.append(float(verts[vi+1]) * ms - oy)
|
||||
verts_scaled.append(float(verts[vi+2]) * ms - oz)
|
||||
# Offset pre-scaled vertices relative to origin
|
||||
n = len(scaled)
|
||||
verts_scaled = [0.0] * n
|
||||
for vi in range(0, n - 2, 3):
|
||||
verts_scaled[vi] = scaled[vi] - ox
|
||||
verts_scaled[vi + 1] = scaled[vi + 1] - oy
|
||||
verts_scaled[vi + 2] = scaled[vi + 2] - oz
|
||||
|
||||
mesh_breps = build_ifc_breps(ifc, verts_scaled, face_groups)
|
||||
mesh_facesets = build_ifc_facesets(ifc, verts_scaled, face_groups)
|
||||
|
||||
if not mesh_breps:
|
||||
if not mesh_facesets:
|
||||
continue
|
||||
|
||||
# Apply material style to every component brep of this mesh
|
||||
# Apply material style to every faceset of this mesh
|
||||
if material_manager:
|
||||
mesh_app_id = _get(mesh, "applicationId")
|
||||
if mesh_app_id:
|
||||
for brep in mesh_breps:
|
||||
material_manager.apply_to_item(brep, str(mesh_app_id))
|
||||
for fs in mesh_facesets:
|
||||
material_manager.apply_to_item(fs, str(mesh_app_id))
|
||||
|
||||
brep_items.extend(mesh_breps)
|
||||
geom_items.extend(mesh_facesets)
|
||||
|
||||
if not brep_items:
|
||||
if not geom_items:
|
||||
return None, None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -457,8 +410,8 @@ def mesh_to_ifc(
|
||||
rep = ifc.createIfcShapeRepresentation(
|
||||
ContextOfItems=body_context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="Brep",
|
||||
Items=brep_items,
|
||||
RepresentationType="Tessellation",
|
||||
Items=geom_items,
|
||||
)
|
||||
placement = _make_placement(ifc, ox, oy, oz)
|
||||
|
||||
|
||||
+226
-97
@@ -15,10 +15,14 @@
|
||||
# Definition geometry lives in root → Collection("definitionGeometry")
|
||||
#
|
||||
# We detect the format by the definitionId prefix.
|
||||
#
|
||||
# Performance: uses IfcRepresentationMap + IfcMappedItem so that all instances
|
||||
# sharing the same definition reference a single copy of the geometry.
|
||||
# =============================================================================
|
||||
|
||||
import math
|
||||
from specklepy.objects.base import Base
|
||||
from utils.geometry import _get, unwrap_chunks, decode_faces, _UNIT_SCALES, build_ifc_breps
|
||||
from utils.geometry import _get, unwrap_chunks, decode_faces, _UNIT_SCALES, build_ifc_facesets
|
||||
|
||||
|
||||
def is_instance(obj) -> bool:
|
||||
@@ -74,9 +78,8 @@ def build_definition_map(root: Base) -> dict:
|
||||
|
||||
# Diagnostic: dump first 3 instanceDefinitionProxies to understand structure
|
||||
print("\n [PROXY DIAG] First 3 instanceDefinitionProxies from root:")
|
||||
proxies_raw2 = _get(root, "instanceDefinitionProxies")
|
||||
if proxies_raw2:
|
||||
sample = proxies_raw2 if isinstance(proxies_raw2, list) else [proxies_raw2]
|
||||
if proxies_raw:
|
||||
sample = proxies_raw if isinstance(proxies_raw, list) else [proxies_raw]
|
||||
for i, proxy in enumerate(sample[:3]):
|
||||
app_id = _get(proxy, "applicationId") or "?"
|
||||
name = _get(proxy, "name") or "?"
|
||||
@@ -213,32 +216,18 @@ def _resolve_instance_scale(obj, stream_scale: float) -> float:
|
||||
return stream_scale
|
||||
|
||||
|
||||
def _parse_transform(t: list, scale: float) -> tuple:
|
||||
"""
|
||||
Row-major 4x4 matrix.
|
||||
Translation at t[3], t[7], t[11] — scaled to metres.
|
||||
Local X axis = row 0, Local Z axis = row 2.
|
||||
"""
|
||||
tx = float(t[3]) * scale
|
||||
ty = float(t[7]) * scale
|
||||
tz = float(t[11]) * scale
|
||||
x_axis = (float(t[0]), float(t[1]), float(t[2]))
|
||||
z_axis = (float(t[8]), float(t[9]), float(t[10]))
|
||||
return (tx, ty, tz), x_axis, z_axis
|
||||
|
||||
|
||||
def _make_ifc_placement(ifc, tx, ty, tz, x_axis, z_axis):
|
||||
origin = ifc.createIfcCartesianPoint([tx, ty, tz])
|
||||
x_dir = ifc.createIfcDirection(list(x_axis))
|
||||
z_dir = ifc.createIfcDirection(list(z_axis))
|
||||
a2p = ifc.createIfcAxis2Placement3D(origin, z_dir, x_dir)
|
||||
return ifc.createIfcLocalPlacement(PlacementRelTo=None, RelativePlacement=a2p)
|
||||
|
||||
|
||||
# Stats
|
||||
_stats = {"found": 0, "not_found": 0}
|
||||
_dbg_cnt = [0]
|
||||
|
||||
# Cache: mesh id → (verts_flat, face_groups, ms) to avoid re-unpacking
|
||||
# the same definition mesh across many instances that share it.
|
||||
_mesh_data_cache: dict = {}
|
||||
|
||||
# Cache: definition_id → IfcRepresentationMap (or None if no geometry)
|
||||
# All instances sharing the same definition reuse one geometry copy.
|
||||
_rep_map_cache: dict = {}
|
||||
|
||||
|
||||
_MM_SCALES = {
|
||||
"mm": 1.0, "millimeter": 1.0, "millimeters": 1.0,
|
||||
@@ -248,28 +237,157 @@ _MM_SCALES = {
|
||||
}
|
||||
|
||||
|
||||
def _apply_transform(t: list, vx: float, vy: float, vz: float, ts: float) -> tuple:
|
||||
"""
|
||||
Apply a row-major 4x4 transform to a single vertex.
|
||||
ts = scale factor applied to the translation components only (not rotation).
|
||||
For Revit mm data with IFC in mm: ts=1.0 (no conversion).
|
||||
For IFC-format transforms (metres): ts=1000.0 (m→mm).
|
||||
Rotation components are dimensionless and never scaled.
|
||||
"""
|
||||
x = t[0]*vx + t[1]*vy + t[2]*vz + t[3] * ts
|
||||
y = t[4]*vx + t[5]*vy + t[6]*vz + t[7] * ts
|
||||
z = t[8]*vx + t[9]*vy + t[10]*vz + t[11] * ts
|
||||
return x, y, z
|
||||
# --------------------------------------------------------------------------- #
|
||||
# IfcRepresentationMap builder — geometry created once per definition
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _build_rep_map(ifc, body_context, meshes: list, ifc_format: bool,
|
||||
material_manager=None):
|
||||
"""
|
||||
Build an IfcRepresentationMap from definition meshes.
|
||||
Geometry is in local coordinates (mm, no instance transform applied).
|
||||
Returns IfcRepresentationMap or None if no valid geometry.
|
||||
"""
|
||||
geom_items = []
|
||||
|
||||
for mesh in meshes:
|
||||
mesh_id = _get(mesh, "id") or _get(mesh, "applicationId")
|
||||
if mesh_id and mesh_id in _mesh_data_cache:
|
||||
verts, face_groups, ms = _mesh_data_cache[mesh_id]
|
||||
else:
|
||||
raw_verts = _get(mesh, "vertices") or []
|
||||
raw_faces = _get(mesh, "faces") or []
|
||||
verts = unwrap_chunks(list(raw_verts))
|
||||
faces_raw = unwrap_chunks(list(raw_faces))
|
||||
if not verts or not faces_raw:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
face_groups = decode_faces(faces_raw)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Instance face decode: {e}")
|
||||
continue
|
||||
|
||||
if mesh_id:
|
||||
_mesh_data_cache[mesh_id] = (verts, face_groups, ms)
|
||||
|
||||
# Scale vertices to mm (local coordinates, no instance transform)
|
||||
verts_local = []
|
||||
for vi in range(0, len(verts) - 2, 3):
|
||||
verts_local.append(float(verts[vi]) * ms)
|
||||
verts_local.append(float(verts[vi+1]) * ms)
|
||||
verts_local.append(float(verts[vi+2]) * ms)
|
||||
|
||||
mesh_facesets = build_ifc_facesets(ifc, verts_local, face_groups)
|
||||
|
||||
if not mesh_facesets:
|
||||
continue
|
||||
|
||||
# Apply material style to each faceset
|
||||
if material_manager:
|
||||
mesh_app_id = _get(mesh, "applicationId")
|
||||
if mesh_app_id:
|
||||
for fs in mesh_facesets:
|
||||
material_manager.apply_to_item(fs, str(mesh_app_id))
|
||||
|
||||
geom_items.extend(mesh_facesets)
|
||||
|
||||
if not geom_items:
|
||||
return None
|
||||
|
||||
# Mapping origin = identity (local coords origin)
|
||||
origin = ifc.createIfcCartesianPoint([0.0, 0.0, 0.0])
|
||||
a2p = ifc.createIfcAxis2Placement3D(origin, None, None)
|
||||
|
||||
# The mapped representation holds the actual geometry
|
||||
mapped_rep = ifc.createIfcShapeRepresentation(
|
||||
ContextOfItems=body_context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="Tessellation",
|
||||
Items=geom_items,
|
||||
)
|
||||
|
||||
return ifc.createIfcRepresentationMap(a2p, mapped_rep)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Transform → IfcCartesianTransformationOperator3D
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _vec_magnitude(x, y, z):
|
||||
return math.sqrt(x*x + y*y + z*z)
|
||||
|
||||
|
||||
def _make_transform_operator(ifc, t: list, ts: float):
|
||||
"""
|
||||
Convert a row-major 4x4 matrix + translation scale into an
|
||||
IfcCartesianTransformationOperator3DnonUniform.
|
||||
|
||||
t: 16 floats, row-major [r00,r01,r02,tx, r10,r11,r12,ty, r20,r21,r22,tz, 0,0,0,1]
|
||||
ts: scale factor for translation components (e.g. 1000.0 for m→mm)
|
||||
|
||||
The matrix acts as: p' = M * p + translation, where M rows are:
|
||||
row0 = (t[0], t[1], t[2])
|
||||
row1 = (t[4], t[5], t[6])
|
||||
row2 = (t[8], t[9], t[10])
|
||||
|
||||
IfcCartesianTransformationOperator axes represent the COLUMNS of M:
|
||||
Axis1 = column 0 = where local X maps → (t[0], t[4], t[8])
|
||||
Axis2 = column 1 = where local Y maps → (t[1], t[5], t[9])
|
||||
Axis3 = column 2 = where local Z maps → (t[2], t[6], t[10])
|
||||
|
||||
Returns the IFC entity, or None if the transform is degenerate.
|
||||
"""
|
||||
# Extract COLUMNS of the 3x3 rotation/scale sub-matrix
|
||||
ax1 = (float(t[0]), float(t[4]), float(t[8])) # column 0: X-axis direction
|
||||
ax2 = (float(t[1]), float(t[5]), float(t[9])) # column 1: Y-axis direction
|
||||
ax3 = (float(t[2]), float(t[6]), float(t[10])) # column 2: Z-axis direction
|
||||
|
||||
s1 = _vec_magnitude(*ax1)
|
||||
s2 = _vec_magnitude(*ax2)
|
||||
s3 = _vec_magnitude(*ax3)
|
||||
|
||||
if s1 < 1e-10 or s2 < 1e-10 or s3 < 1e-10:
|
||||
return None # degenerate transform
|
||||
|
||||
# Normalized direction vectors
|
||||
d1 = ifc.createIfcDirection([ax1[0]/s1, ax1[1]/s1, ax1[2]/s1])
|
||||
d2 = ifc.createIfcDirection([ax2[0]/s2, ax2[1]/s2, ax2[2]/s2])
|
||||
d3 = ifc.createIfcDirection([ax3[0]/s3, ax3[1]/s3, ax3[2]/s3])
|
||||
|
||||
# Translation, scaled to mm
|
||||
tx = float(t[3]) * ts
|
||||
ty = float(t[7]) * ts
|
||||
tz = float(t[11]) * ts
|
||||
origin = ifc.createIfcCartesianPoint([tx, ty, tz])
|
||||
|
||||
# Use non-uniform variant to handle mirrors and non-uniform scale
|
||||
return ifc.createIfcCartesianTransformationOperator3DnonUniform(
|
||||
d1, # Axis1
|
||||
d2, # Axis2
|
||||
origin, # LocalOrigin
|
||||
s1, # Scale
|
||||
d3, # Axis3
|
||||
s2, # Scale2
|
||||
s3, # Scale3
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Main conversion — IfcMappedItem approach
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def instance_to_ifc(ifc, body_context, obj: Base, definition_map: dict,
|
||||
scale: float = 1.0, material_manager=None):
|
||||
"""
|
||||
Convert a Speckle InstanceProxy → (IfcShapeRepresentation, IfcLocalPlacement).
|
||||
|
||||
Strategy: BAKE the full 4x4 transform into every vertex (world coordinates).
|
||||
Creates one IfcFacetedBrep per definition mesh so each can carry its own
|
||||
material style via renderMaterialProxies.
|
||||
Strategy: create geometry once per definition as an IfcRepresentationMap,
|
||||
then reference it via IfcMappedItem + IfcCartesianTransformationOperator3D
|
||||
for each instance. This avoids duplicating geometry across instances.
|
||||
"""
|
||||
transform_raw = _get(obj, "transform")
|
||||
if not transform_raw:
|
||||
@@ -293,81 +411,92 @@ def instance_to_ifc(ifc, body_context, obj: Base, definition_map: dict,
|
||||
print(f" [INST {_dbg_cnt[0]} {fmt}] {definition_id[:40]}")
|
||||
print(f" t[3]={t[3]:.1f} t[7]={t[7]:.1f} t[11]={t[11]:.1f} x={x_axis} z={z_axis}")
|
||||
|
||||
# World-origin placement (geometry is baked to world coords)
|
||||
# Identity placement (transform is encoded in the MappedItem)
|
||||
origin = ifc.createIfcCartesianPoint([0.0, 0.0, 0.0])
|
||||
a2p = ifc.createIfcAxis2Placement3D(origin, None, None)
|
||||
placement = ifc.createIfcLocalPlacement(PlacementRelTo=None, RelativePlacement=a2p)
|
||||
|
||||
# Get definition meshes
|
||||
if ifc_format:
|
||||
meshes = _get_ifc_meshes(definition_id, definition_map)
|
||||
# --- Get or build IfcRepresentationMap (cached per definition_id) ---
|
||||
if definition_id not in _rep_map_cache:
|
||||
if ifc_format:
|
||||
meshes = _get_ifc_meshes(definition_id, definition_map)
|
||||
else:
|
||||
meshes = _get_revit_meshes(definition_id, definition_map)
|
||||
|
||||
if not meshes:
|
||||
_stats["not_found"] += 1
|
||||
_rep_map_cache[definition_id] = None
|
||||
return None, placement
|
||||
|
||||
_stats["found"] += 1
|
||||
_rep_map_cache[definition_id] = _build_rep_map(
|
||||
ifc, body_context, meshes, ifc_format, material_manager
|
||||
)
|
||||
else:
|
||||
meshes = _get_revit_meshes(definition_id, definition_map)
|
||||
# Track stats even for cached definitions
|
||||
if _rep_map_cache[definition_id] is not None:
|
||||
_stats["found"] += 1
|
||||
else:
|
||||
_stats["not_found"] += 1
|
||||
|
||||
if not meshes:
|
||||
_stats["not_found"] += 1
|
||||
rep_map = _rep_map_cache[definition_id]
|
||||
if rep_map is None:
|
||||
return None, placement
|
||||
|
||||
_stats["found"] += 1
|
||||
|
||||
# One brep per mesh so each can have its own material style
|
||||
brep_items = []
|
||||
for mesh in meshes:
|
||||
raw_verts = _get(mesh, "vertices") or []
|
||||
raw_faces = _get(mesh, "faces") or []
|
||||
verts = unwrap_chunks(list(raw_verts))
|
||||
faces_raw = unwrap_chunks(list(raw_faces))
|
||||
if not verts or not faces_raw:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
face_groups = decode_faces(faces_raw)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Instance face decode: {e}")
|
||||
continue
|
||||
|
||||
# Pre-compute world coords for all vertices in this mesh
|
||||
verts_world = []
|
||||
for vi in range(0, len(verts) - 2, 3):
|
||||
lx = float(verts[vi]) * ms
|
||||
ly = float(verts[vi+1]) * ms
|
||||
lz = float(verts[vi+2]) * ms
|
||||
wx, wy, wz = _apply_transform(t, lx, ly, lz, ts)
|
||||
verts_world.append(wx)
|
||||
verts_world.append(wy)
|
||||
verts_world.append(wz)
|
||||
|
||||
mesh_breps = build_ifc_breps(ifc, verts_world, face_groups)
|
||||
|
||||
if not mesh_breps:
|
||||
continue
|
||||
|
||||
# Apply material style to every component brep of this mesh
|
||||
if material_manager:
|
||||
mesh_app_id = _get(mesh, "applicationId")
|
||||
if mesh_app_id:
|
||||
for brep in mesh_breps:
|
||||
material_manager.apply_to_item(brep, str(mesh_app_id))
|
||||
|
||||
brep_items.extend(mesh_breps)
|
||||
|
||||
if not brep_items:
|
||||
# --- Build transform operator from instance's 4x4 matrix ---
|
||||
transform_op = _make_transform_operator(ifc, t, ts)
|
||||
if transform_op is None:
|
||||
return None, placement
|
||||
|
||||
# --- Create IfcMappedItem referencing the shared geometry ---
|
||||
mapped_item = ifc.createIfcMappedItem(rep_map, transform_op)
|
||||
|
||||
rep = ifc.createIfcShapeRepresentation(
|
||||
ContextOfItems=body_context,
|
||||
RepresentationIdentifier="Body",
|
||||
RepresentationType="Brep",
|
||||
Items=brep_items,
|
||||
RepresentationType="MappedRepresentation",
|
||||
Items=[mapped_item],
|
||||
)
|
||||
return rep, placement
|
||||
|
||||
|
||||
def get_definition_object(obj: Base, definition_map: dict):
|
||||
"""
|
||||
Resolve the definition's source object for an InstanceProxy.
|
||||
Returns the first object referenced by the definition proxy, which
|
||||
carries the proper category/type info. Returns None if not found.
|
||||
"""
|
||||
definition_id = _get(obj, "definitionId") or ""
|
||||
if not definition_id:
|
||||
return None
|
||||
|
||||
ifc_proxies = definition_map.get("ifc_proxies", {})
|
||||
proxy = ifc_proxies.get(definition_id) or ifc_proxies.get(definition_id.lower())
|
||||
if proxy is None:
|
||||
return None
|
||||
|
||||
object_ids = _get(proxy, "objects") or []
|
||||
if not isinstance(object_ids, list):
|
||||
object_ids = list(object_ids)
|
||||
if not object_ids:
|
||||
return None
|
||||
|
||||
by_app_id = definition_map.get("by_app_id", {})
|
||||
source = by_app_id.get(str(object_ids[0]).lower())
|
||||
return source
|
||||
|
||||
|
||||
def print_instance_stats():
|
||||
total = _stats["found"] + _stats["not_found"]
|
||||
print(f" Instance resolution: {_stats['found']}/{total} definitions found")
|
||||
if _stats["not_found"] > 0:
|
||||
print(f" ⚠️ {_stats['not_found']} instances had no definition geometry")
|
||||
|
||||
|
||||
def reset_caches():
|
||||
"""Reset module-level caches (call at start of each export run)."""
|
||||
_mesh_data_cache.clear()
|
||||
_rep_map_cache.clear()
|
||||
_stats["found"] = 0
|
||||
_stats["not_found"] = 0
|
||||
_dbg_cnt[0] = 0
|
||||
|
||||
+226
-43
@@ -1,16 +1,131 @@
|
||||
# =============================================================================
|
||||
# mapper.py
|
||||
# Maps Speckle speckle_type strings and Revit category names → IFC entity classes.
|
||||
# Maps Speckle objects → IFC entity classes.
|
||||
#
|
||||
# Strategy:
|
||||
# 1. Try to match speckle_type exactly or by prefix
|
||||
# 2. Fall back to Revit category name (e.g. "Floors" → IfcSlab)
|
||||
# 3. Fall back to IfcBuildingElementProxy if nothing matches
|
||||
# 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
|
||||
#
|
||||
# builtInCategory values: https://www.revitapidocs.com/2019/ba1c5b30-242f-5fdc-8ea9-ec3b61e6e722.htm
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# --- speckle_type → IFC class ---
|
||||
# Covers Objects.BuiltElements.* from the Speckle Objects kit
|
||||
# --- OST_ BuiltInCategory → IFC class (primary lookup) ---
|
||||
BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
# Architectural - Walls
|
||||
"OST_Walls": "IfcWall",
|
||||
"OST_CurtainWallPanels": "IfcCurtainWall",
|
||||
"OST_CurtainWallMullions": "IfcMember",
|
||||
"OST_Fascia": "IfcCovering",
|
||||
"OST_Gutters": "IfcPipeSegment",
|
||||
|
||||
# Architectural - Floors / Roofs / Ceilings
|
||||
"OST_Floors": "IfcSlab",
|
||||
"OST_Roofs": "IfcRoof",
|
||||
"OST_Ceilings": "IfcCovering",
|
||||
"OST_RoofSoffit": "IfcCovering",
|
||||
|
||||
# Architectural - Doors / Windows / Openings
|
||||
"OST_Doors": "IfcDoor",
|
||||
"OST_Windows": "IfcWindow",
|
||||
"OST_CurtainWallFamilies": "IfcCurtainWall",
|
||||
"OST_Skylights": "IfcWindow",
|
||||
|
||||
# Architectural - Stairs / Ramps / Railings
|
||||
"OST_Stairs": "IfcStair",
|
||||
"OST_StairsRailing": "IfcRailing",
|
||||
"OST_RailingTopRail": "IfcRailing",
|
||||
"OST_Ramps": "IfcRamp",
|
||||
"OST_StairsLandings": "IfcStairFlight",
|
||||
"OST_StairsRuns": "IfcStairFlight",
|
||||
"OST_StairsSupports": "IfcMember",
|
||||
|
||||
# Architectural - Rooms / Spaces
|
||||
"OST_Rooms": "IfcSpace",
|
||||
"OST_Parking": "IfcSpace",
|
||||
"OST_Areas": "IfcSpace",
|
||||
|
||||
# Architectural - Furniture / Casework
|
||||
"OST_Furniture": "IfcFurnishingElement",
|
||||
"OST_FurnitureSystems": "IfcFurnishingElement",
|
||||
"OST_Casework": "IfcFurnishingElement",
|
||||
"OST_SpecialtyEquipment": "IfcFurnishingElement",
|
||||
"OST_Entourage": "IfcFurnishingElement",
|
||||
|
||||
# Structural
|
||||
"OST_StructuralColumns": "IfcColumn",
|
||||
"OST_Columns": "IfcColumn",
|
||||
"OST_StructuralFraming": "IfcBeam",
|
||||
"OST_StructuralFoundation": "IfcFooting",
|
||||
"OST_FoundationSlab": "IfcSlab",
|
||||
"OST_StructuralStiffener": "IfcMember",
|
||||
"OST_StructuralTruss": "IfcMember",
|
||||
"OST_StructuralConnectionModel": "IfcMechanicalFastener",
|
||||
"OST_Rebar": "IfcReinforcingBar",
|
||||
"OST_FabricAreas": "IfcReinforcingMesh",
|
||||
"OST_FabricReinforcement": "IfcReinforcingMesh",
|
||||
|
||||
# MEP - HVAC
|
||||
"OST_DuctCurves": "IfcDuctSegment",
|
||||
"OST_DuctFitting": "IfcDuctFitting",
|
||||
"OST_DuctAccessory": "IfcDuctSegment",
|
||||
"OST_DuctTerminal": "IfcAirTerminal",
|
||||
"OST_FlexDuctCurves": "IfcDuctSegment",
|
||||
"OST_MechanicalEquipment": "IfcUnitaryEquipment",
|
||||
"OST_AirTerminal": "IfcAirTerminal",
|
||||
|
||||
# MEP - Plumbing
|
||||
"OST_PipeCurves": "IfcPipeSegment",
|
||||
"OST_PipeFitting": "IfcPipeFitting",
|
||||
"OST_PipeAccessory": "IfcPipeSegment",
|
||||
"OST_FlexPipeCurves": "IfcPipeSegment",
|
||||
"OST_PlumbingFixtures": "IfcSanitaryTerminal",
|
||||
"OST_Sprinklers": "IfcFireSuppressionTerminal",
|
||||
|
||||
# MEP - Electrical
|
||||
"OST_ElectricalEquipment": "IfcElectricDistributionBoard",
|
||||
"OST_ElectricalFixtures": "IfcElectricAppliance",
|
||||
"OST_LightingFixtures": "IfcLightFixture",
|
||||
"OST_LightingDevices": "IfcLightFixture",
|
||||
"OST_CableTray": "IfcCableCarrierSegment",
|
||||
"OST_CableTrayFitting": "IfcCableCarrierFitting",
|
||||
"OST_Conduit": "IfcCableCarrierSegment",
|
||||
"OST_ConduitFitting": "IfcCableCarrierFitting",
|
||||
"OST_CommunicationDevices": "IfcElectricAppliance",
|
||||
"OST_DataDevices": "IfcElectricAppliance",
|
||||
"OST_FireAlarmDevices": "IfcAlarm",
|
||||
"OST_SecurityDevices": "IfcAlarm",
|
||||
"OST_NurseCallDevices": "IfcElectricAppliance",
|
||||
|
||||
# Site / Civil
|
||||
"OST_Site": "IfcSite",
|
||||
"OST_Topography": "IfcGeographicElement",
|
||||
"OST_Toposolid": "IfcGeographicElement",
|
||||
"OST_Roads": "IfcRoad",
|
||||
"OST_Hardscape": "IfcPavement",
|
||||
"OST_Planting": "IfcGeographicElement",
|
||||
"OST_SiteSurface": "IfcGeographicElement",
|
||||
|
||||
# Generic / Annotation (skip or proxy)
|
||||
"OST_GenericModel": "IfcBuildingElementProxy",
|
||||
"OST_Mass": "IfcBuildingElementProxy",
|
||||
"OST_DetailComponents": "IfcAnnotation",
|
||||
"OST_Lines": "IfcAnnotation",
|
||||
"OST_Grids": "IfcGrid",
|
||||
"OST_Levels": "IfcBuildingStorey",
|
||||
"OST_Views": "IfcAnnotation",
|
||||
}
|
||||
|
||||
|
||||
# --- OST_ BuiltInCategory → PredefinedType (where applicable) ---
|
||||
BUILTIN_PREDEFINED_TYPE: dict[str, str] = {
|
||||
"OST_RailingTopRail": "HANDRAIL",
|
||||
}
|
||||
|
||||
|
||||
# --- speckle_type → IFC class (secondary lookup) ---
|
||||
SPECKLE_TYPE_MAP: dict[str, str] = {
|
||||
"Objects.BuiltElements.Wall": "IfcWall",
|
||||
"Objects.BuiltElements.Floor": "IfcSlab",
|
||||
@@ -47,7 +162,7 @@ SPECKLE_TYPE_MAP: dict[str, str] = {
|
||||
"Objects.Geometry.Brep": "IfcBuildingElementProxy",
|
||||
}
|
||||
|
||||
# --- Revit category name → IFC class (fallback) ---
|
||||
# --- Display category name → IFC class (tertiary fallback) ---
|
||||
CATEGORY_MAP: dict[str, str] = {
|
||||
"Walls": "IfcWall",
|
||||
"Floors": "IfcSlab",
|
||||
@@ -66,6 +181,7 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
"Stairs": "IfcStair",
|
||||
"Ramps": "IfcRamp",
|
||||
"Railings": "IfcRailing",
|
||||
"Top Rails": "IfcRailing",
|
||||
"Curtain Panels": "IfcCurtainWall",
|
||||
"Curtain Wall Mullions": "IfcMember",
|
||||
"Doors": "IfcDoor",
|
||||
@@ -81,57 +197,124 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
"Structural Foundations": "IfcFooting",
|
||||
"Foundation Slabs": "IfcSlab",
|
||||
"Topography": "IfcGeographicElement",
|
||||
"Toposolid": "IfcGeographicElement",
|
||||
"Planting": "IfcGeographicElement",
|
||||
"Site": "IfcSite",
|
||||
"Parking": "IfcSpace",
|
||||
"Generic Models": "IfcBuildingElementProxy",
|
||||
"Mass": "IfcBuildingElementProxy",
|
||||
"Specialty Equipment": "IfcBuildingElementProxy",
|
||||
"Specialty Equipment": "IfcFurnishingElement",
|
||||
}
|
||||
|
||||
|
||||
def get_predefined_type(obj) -> str | None:
|
||||
"""Return the IFC PredefinedType for an object based on its builtInCategory, or None."""
|
||||
bic = _get_builtin_category(obj)
|
||||
if bic and bic in BUILTIN_PREDEFINED_TYPE:
|
||||
return BUILTIN_PREDEFINED_TYPE[bic]
|
||||
return None
|
||||
|
||||
|
||||
_bic_cache: dict[int, str | None] = {} # id(obj) → builtInCategory
|
||||
|
||||
|
||||
def _get_builtin_category(obj) -> str | None:
|
||||
"""
|
||||
Read builtInCategory from obj.properties.builtInCategory.
|
||||
Returns the OST_ string or None. Cached per object.
|
||||
"""
|
||||
oid = id(obj)
|
||||
if oid in _bic_cache:
|
||||
return _bic_cache[oid]
|
||||
result = None
|
||||
try:
|
||||
props = getattr(obj, "properties", None)
|
||||
if props is None:
|
||||
try:
|
||||
props = obj["properties"]
|
||||
except Exception:
|
||||
pass
|
||||
if props is not None:
|
||||
val = getattr(props, "builtInCategory", None)
|
||||
if val is None:
|
||||
try:
|
||||
val = props["builtInCategory"]
|
||||
except Exception:
|
||||
pass
|
||||
if val and isinstance(val, str):
|
||||
result = val.strip()
|
||||
except Exception:
|
||||
pass
|
||||
_bic_cache[oid] = result
|
||||
return result
|
||||
|
||||
|
||||
# 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()
|
||||
]
|
||||
|
||||
# Classification cache: (obj_id, category_name) → ifc_class
|
||||
_classify_cache: dict[tuple, str] = {}
|
||||
|
||||
|
||||
def classify(obj, category_name: str = "") -> str:
|
||||
"""
|
||||
Determine the IFC class for a Speckle object.
|
||||
|
||||
With the new Objects.Data.DataObject:Objects.Data.RevitObject speckle_type,
|
||||
category name is now the primary classification signal.
|
||||
|
||||
Args:
|
||||
obj: A specklepy Base object (leaf element).
|
||||
category_name: The Revit category string from the traversal context
|
||||
e.g. "Floors", "Walls", "Structural Columns"
|
||||
|
||||
Returns:
|
||||
An IFC class name string e.g. "IfcWall"
|
||||
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
|
||||
"""
|
||||
cache_key = (id(obj), category_name)
|
||||
if cache_key in _classify_cache:
|
||||
return _classify_cache[cache_key]
|
||||
|
||||
result = _classify_impl(obj, category_name)
|
||||
_classify_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
|
||||
def _classify_impl(obj, category_name: str) -> str:
|
||||
# 1. builtInCategory — most reliable, direct Revit enum
|
||||
bic = _get_builtin_category(obj)
|
||||
if bic and bic in BUILTIN_CATEGORY_MAP:
|
||||
return BUILTIN_CATEGORY_MAP[bic]
|
||||
|
||||
# 2. speckle_type
|
||||
speckle_type = getattr(obj, "speckle_type", "") or ""
|
||||
|
||||
# 1. Category name — PRIMARY lookup for RevitObject types
|
||||
if category_name:
|
||||
# Exact match
|
||||
if category_name in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[category_name]
|
||||
# Partial match handles Revit appending IDs e.g. "Structural Framing [12345]"
|
||||
for key, ifc_class in CATEGORY_MAP.items():
|
||||
if key.lower() in category_name.lower():
|
||||
return ifc_class
|
||||
|
||||
# 2. Read 'category' directly off the object itself
|
||||
# Per docs: category is a TOP-LEVEL field on RevitObject, not inside properties
|
||||
obj_category = getattr(obj, "category", None)
|
||||
if obj_category and isinstance(obj_category, str):
|
||||
if obj_category in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[obj_category]
|
||||
for key, ifc_class in CATEGORY_MAP.items():
|
||||
if key.lower() in obj_category.lower():
|
||||
return ifc_class
|
||||
|
||||
# 3. speckle_type — fallback for non-RevitObject types (geometry, structural, etc.)
|
||||
if speckle_type in SPECKLE_TYPE_MAP:
|
||||
return SPECKLE_TYPE_MAP[speckle_type]
|
||||
for key, ifc_class in SPECKLE_TYPE_MAP.items():
|
||||
if speckle_type.startswith(key):
|
||||
return ifc_class
|
||||
|
||||
# 4. Last resort
|
||||
return "IfcBuildingElementProxy"
|
||||
# 3. category_name from traversal context
|
||||
if category_name:
|
||||
if category_name in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[category_name]
|
||||
cat_lower = category_name.lower()
|
||||
for key_lower, ifc_class in _CATEGORY_MAP_LOWER:
|
||||
if key_lower in cat_lower:
|
||||
return ifc_class
|
||||
|
||||
# 4. obj.category field
|
||||
obj_category = getattr(obj, "category", None)
|
||||
if obj_category and isinstance(obj_category, str):
|
||||
if obj_category in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[obj_category]
|
||||
obj_cat_lower = obj_category.lower()
|
||||
for key_lower, ifc_class in _CATEGORY_MAP_LOWER:
|
||||
if key_lower in obj_cat_lower:
|
||||
return ifc_class
|
||||
|
||||
return "IfcBuildingElementProxy"
|
||||
|
||||
|
||||
def reset_caches():
|
||||
"""Clear module-level caches (call at start of each export run)."""
|
||||
_bic_cache.clear()
|
||||
_classify_cache.clear()
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ class MaterialManager:
|
||||
return style
|
||||
|
||||
def apply_to_item(self, item, mesh_app_id: str):
|
||||
"""Assign the material style to a single IFC geometry item (e.g. IfcFacetedBrep)."""
|
||||
"""Assign the material style to a single IFC geometry item (e.g. IfcPolygonalFaceSet)."""
|
||||
style = self.get_style(mesh_app_id)
|
||||
if style is None:
|
||||
return
|
||||
|
||||
+615
-138
@@ -1,177 +1,654 @@
|
||||
# =============================================================================
|
||||
# properties.py
|
||||
# Extracts Revit data from Speckle DataObjects and writes IFC property sets.
|
||||
# Writes IFC property sets matching the structure of Revit's native IFC export.
|
||||
#
|
||||
# Revit parameter structure from the Speckle connector:
|
||||
# obj.properties = {
|
||||
# "elementId": "704282",
|
||||
# "Parameters": {
|
||||
# "Type Parameters": {
|
||||
# "Dimensions": {
|
||||
# "Thickness": {"name": "Thickness", "value": 25.4, "units": "Millimeters", ...}
|
||||
# },
|
||||
# ...
|
||||
# },
|
||||
# "Instance Parameters": {
|
||||
# "Constraints": {
|
||||
# "Level": {"name": "Level", "value": "Level 1", ...}
|
||||
# },
|
||||
# ...
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# Revit native IFC export produces:
|
||||
# - Element Name: "Family:TypeName:ElementId" e.g. "Basic Roof:SG Metal Panels roof:243274"
|
||||
# - Element Tag: ElementId string e.g. "243274"
|
||||
# - Element GlobalId: from IFC Parameters.IfcGUID
|
||||
# - Pset_<EntityType>Common with typed properties (IfcBoolean, IfcIdentifier, etc.)
|
||||
# - Pset_EnvironmentalImpactIndicators with Reference = TypeName
|
||||
#
|
||||
# We flatten this into two IFC property sets:
|
||||
# Pset_RevitTypeParameters — from "Type Parameters"
|
||||
# Pset_RevitInstanceParameters — from "Instance Parameters"
|
||||
# Our Speckle source fields:
|
||||
# obj.family → Family name
|
||||
# obj.type → Type name (= Reference in all Common psets)
|
||||
# properties.elementId → Revit ElementId → Tag
|
||||
# properties.Parameters.Instance Parameters.IFC Parameters.IfcGUID.value → GlobalId
|
||||
# properties.Parameters.Type Parameters.* → typed IFC properties
|
||||
# properties.Parameters.Instance Parameters.* → typed IFC properties
|
||||
# =============================================================================
|
||||
|
||||
import ifcopenshell.api
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
def _safe_val(value) -> str | None:
|
||||
"""Convert a value to a clean IFC-safe string."""
|
||||
# ---------------------------------------------------------------------------
|
||||
# IFC entity → standard Common pset name
|
||||
# ---------------------------------------------------------------------------
|
||||
COMMON_PSET: dict[str, str] = {
|
||||
"IfcWall": "Pset_WallCommon",
|
||||
"IfcWallStandardCase": "Pset_WallCommon",
|
||||
"IfcSlab": "Pset_SlabCommon",
|
||||
"IfcRoof": "Pset_RoofCommon",
|
||||
"IfcColumn": "Pset_ColumnCommon",
|
||||
"IfcBeam": "Pset_BeamCommon",
|
||||
"IfcMember": "Pset_MemberCommon",
|
||||
"IfcDoor": "Pset_DoorCommon",
|
||||
"IfcWindow": "Pset_WindowCommon",
|
||||
"IfcStair": "Pset_StairCommon",
|
||||
"IfcStairFlight": "Pset_StairFlightCommon",
|
||||
"IfcRamp": "Pset_RampCommon",
|
||||
"IfcRailing": "Pset_RailingCommon",
|
||||
"IfcCovering": "Pset_CoveringCommon",
|
||||
"IfcCurtainWall": "Pset_CurtainWallCommon",
|
||||
"IfcFooting": "Pset_FootingCommon",
|
||||
"IfcPile": "Pset_PileCommon",
|
||||
"IfcSpace": "Pset_SpaceCommon",
|
||||
"IfcSite": "Pset_SiteCommon",
|
||||
"IfcBuildingStorey": "Pset_BuildingStoreyCommon",
|
||||
"IfcBuilding": "Pset_BuildingCommon",
|
||||
"IfcBuildingElementProxy": "Pset_BuildingElementProxyCommon",
|
||||
"IfcFurnishingElement": "Pset_FurnitureTypeCommon",
|
||||
"IfcLightFixture": "Pset_LightFixtureTypeCommon",
|
||||
"IfcOpeningElement": "Pset_OpeningElementCommon",
|
||||
"IfcPlate": "Pset_PlateCommon",
|
||||
"IfcGeographicElement": "Pset_SiteCommon",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Revit parameter internal names → (IFC pset property name, IFC value factory)
|
||||
# These are harvested from the Common psets Revit native export produces.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _bool(v):
|
||||
return ("IfcBoolean", bool(v))
|
||||
|
||||
def _identifier(v):
|
||||
return ("IfcIdentifier", str(v))
|
||||
|
||||
def _label(v):
|
||||
return ("IfcLabel", str(v))
|
||||
|
||||
def _real(v):
|
||||
return ("IfcReal", float(v))
|
||||
|
||||
def _thermal(v):
|
||||
return ("IfcThermalTransmittanceMeasure", float(v))
|
||||
|
||||
def _length(v):
|
||||
return ("IfcPositiveLengthMeasure", float(v))
|
||||
|
||||
def _count(v):
|
||||
return ("IfcCountMeasure", int(v))
|
||||
|
||||
def _angle(v):
|
||||
return ("IfcPlaneAngleMeasure", float(v))
|
||||
|
||||
|
||||
# Map: Revit internalDefinitionName → (IFC property name, value factory fn)
|
||||
REVIT_PARAM_TO_IFC: dict[str, tuple] = {
|
||||
# Wall
|
||||
"WALL_ATTR_ROOM_BOUNDING": ("IsExternal", _bool),
|
||||
"WALL_STRUCTURAL_SIGNIFICANT": ("LoadBearing", _bool),
|
||||
"WALL_STRUCTURAL_USAGE_PARAM": ("LoadBearing", _bool),
|
||||
"ANALYTICAL_THERMAL_RESISTANCE": ("ThermalTransmittance", _thermal),
|
||||
"ANALYTICAL_HEAT_TRANSFER_COEFFICIENT": ("ThermalTransmittance", _thermal),
|
||||
|
||||
# Slab / Roof / Floor
|
||||
"HOST_AREA_COMPUTED": ("NetArea", _real),
|
||||
"HOST_VOLUME_COMPUTED": ("NetVolume", _real),
|
||||
"ROOF_SLOPE": ("PitchAngle", _angle),
|
||||
|
||||
# Stair
|
||||
"STAIR_RISER_HEIGHT": ("RiserHeight", _length),
|
||||
"STAIR_TREAD_DEPTH": ("TreadLength", _length),
|
||||
"STAIR_NUMBER_OF_RISERS": ("NumberOfRiser", _count),
|
||||
"STAIR_NUMBER_OF_TREADS": ("NumberOfTreads", _count),
|
||||
"STAIR_NOSING_LENGTH": ("NosingLength", _length),
|
||||
|
||||
# Railing
|
||||
"RAILING_HEIGHT": ("Height", _length),
|
||||
|
||||
# Door / Window
|
||||
"DOOR_FIRE_RATING": ("FireExit", _bool),
|
||||
|
||||
# General identity
|
||||
"ALL_MODEL_FAMILY_NAME": ("Reference", _identifier),
|
||||
"ALL_MODEL_TYPE_NAME": ("Reference", _identifier),
|
||||
"ASSEMBLY_CODE": ("Reference", _identifier),
|
||||
}
|
||||
|
||||
# External category OST_ codes (used to infer IsExternal)
|
||||
EXTERNAL_CATEGORIES = {
|
||||
"OST_Walls", "OST_Roofs", "OST_Windows", "OST_Doors",
|
||||
"OST_CurtainWallPanels", "OST_CurtainWallMullions",
|
||||
"OST_StructuralColumns", "OST_StructuralFraming",
|
||||
"OST_Stairs", "OST_StairsRailing", "OST_Ramps",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_props_cache: dict[int, dict] = {} # id(obj) → props dict
|
||||
|
||||
|
||||
def _get_props_dict(obj: Base) -> dict:
|
||||
"""Get properties as a plain dict. Cached per object to avoid repeated conversion."""
|
||||
oid = id(obj)
|
||||
if oid in _props_cache:
|
||||
return _props_cache[oid]
|
||||
# Try getattr first — matches the pattern that works in other Speckle scripts
|
||||
p = getattr(obj, "properties", None)
|
||||
if p is None:
|
||||
for key in ["properties", "@properties"]:
|
||||
try:
|
||||
p = obj[key]
|
||||
if p is not None:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if p is None:
|
||||
_props_cache[oid] = {}
|
||||
return {}
|
||||
result = _to_dict(p)
|
||||
_props_cache[oid] = result
|
||||
return result
|
||||
|
||||
|
||||
def _get_nested(d, *keys):
|
||||
"""Safely walk nested dicts/objects."""
|
||||
cur = d
|
||||
for k in keys:
|
||||
if cur is None:
|
||||
return None
|
||||
cur = _safe_get(cur, k)
|
||||
return cur
|
||||
|
||||
|
||||
_to_dict_cache: dict[int, dict] = {} # id(obj) → converted dict
|
||||
|
||||
|
||||
def _to_dict(obj) -> dict:
|
||||
"""Convert a Speckle Base object or dict to a plain dict. Returns {} on failure.
|
||||
Cached per object identity to avoid repeated conversion."""
|
||||
if obj is None:
|
||||
return {}
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
oid = id(obj)
|
||||
if oid in _to_dict_cache:
|
||||
return _to_dict_cache[oid]
|
||||
# Try .get_dynamic_member_names() for Speckle Base objects
|
||||
if hasattr(obj, "get_dynamic_member_names"):
|
||||
result = {}
|
||||
try:
|
||||
names = obj.get_dynamic_member_names()
|
||||
except Exception:
|
||||
_to_dict_cache[oid] = {}
|
||||
return {}
|
||||
for n in names:
|
||||
try:
|
||||
result[n] = obj[n]
|
||||
except Exception:
|
||||
pass
|
||||
_to_dict_cache[oid] = result
|
||||
return result
|
||||
# Last resort: try common dict-like patterns
|
||||
if hasattr(obj, "items"):
|
||||
try:
|
||||
result = dict(obj.items())
|
||||
_to_dict_cache[oid] = result
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
_to_dict_cache[oid] = {}
|
||||
return {}
|
||||
|
||||
|
||||
def _safe_get(obj, key, default=None):
|
||||
"""Safe key access for both dicts and Speckle Base objects."""
|
||||
if obj is None:
|
||||
return default
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
# Try getattr first (works reliably for Speckle Base)
|
||||
try:
|
||||
val = getattr(obj, key, None)
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback to bracket access
|
||||
try:
|
||||
val = obj[key]
|
||||
if val is not None:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
def _param_value(params_block, internal_name: str):
|
||||
"""
|
||||
Search all groups in a parameter block for a param with the given
|
||||
internalDefinitionName. Returns the raw value or None.
|
||||
Handles both plain dicts and Speckle Base objects.
|
||||
"""
|
||||
block = _to_dict(params_block)
|
||||
if not block:
|
||||
return None
|
||||
for group in block.values():
|
||||
group_d = _to_dict(group)
|
||||
if not group_d:
|
||||
continue
|
||||
for entry in group_d.values():
|
||||
entry_d = _to_dict(entry)
|
||||
if not entry_d:
|
||||
continue
|
||||
if entry_d.get("internalDefinitionName") == internal_name:
|
||||
return entry_d.get("value")
|
||||
return None
|
||||
|
||||
|
||||
def _make_prop(ifc, name: str, ifc_type: str, value) -> object | None:
|
||||
"""Create an IfcPropertySingleValue with the correct IFC measure type."""
|
||||
try:
|
||||
nominal = ifc.create_entity(ifc_type, wrappedValue=value)
|
||||
return ifc.create_entity(
|
||||
"IfcPropertySingleValue",
|
||||
Name=name,
|
||||
NominalValue=nominal,
|
||||
)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def _write_pset(ifc, element, pset_name: str, props: list):
|
||||
"""Write an IfcPropertySet with the given list of IfcProperty objects."""
|
||||
if not props:
|
||||
return
|
||||
try:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name=pset_name)
|
||||
# Directly attach the pre-built property objects
|
||||
pset.HasProperties = props
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {pset_name}: {e}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Element name + tag (matching Revit native IFC format)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_element_name(obj: Base) -> str:
|
||||
"""
|
||||
Build element name in Revit native IFC format: "Family:TypeName:ElementId"
|
||||
Falls back gracefully if any part is missing.
|
||||
"""
|
||||
family = getattr(obj, "family", None) or ""
|
||||
typ = getattr(obj, "type", None) or ""
|
||||
|
||||
# Treat literal "none" (case-insensitive) the same as empty — Revit exports
|
||||
# placeholder objects with family/type set to the string "none".
|
||||
if family.strip().lower() == "none":
|
||||
family = ""
|
||||
if typ.strip().lower() == "none":
|
||||
typ = ""
|
||||
|
||||
parts = [p for p in [family, typ] if p]
|
||||
return ":".join(parts) if parts else (getattr(obj, "id", None) or "unnamed")
|
||||
|
||||
|
||||
def get_element_tag(obj: Base) -> str | None:
|
||||
"""Return Revit ElementId as the IFC Tag."""
|
||||
props = _get_props_dict(obj)
|
||||
elem_id = _safe_get(props, "elementId")
|
||||
return str(elem_id) if elem_id else None
|
||||
|
||||
|
||||
def get_ifc_guid(obj: Base) -> str | None:
|
||||
"""
|
||||
Read IfcGUID from the Revit IFC Parameters.
|
||||
Falls back to None (ifcopenshell will auto-generate a GUID).
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
inst = _safe_get(params, "Instance Parameters", {})
|
||||
ifc_p = _safe_get(inst, "IFC Parameters", {})
|
||||
entry = _safe_get(ifc_p, "IfcGUID", {})
|
||||
entry_d = _to_dict(entry) if not isinstance(entry, dict) else entry
|
||||
val = entry_d.get("value") if entry_d else None
|
||||
return str(val) if val else None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Standard Common pset (Pset_WallCommon etc.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_common_pset(ifc, element, obj: Base, ifc_class: str, category_name: str = ""):
|
||||
"""
|
||||
Write the standard Pset_<Entity>Common property set, matching Revit native export.
|
||||
Properties: Reference (TypeName), IsExternal, LoadBearing, ThermalTransmittance, etc.
|
||||
"""
|
||||
pset_name = COMMON_PSET.get(ifc_class)
|
||||
if not pset_name:
|
||||
return
|
||||
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
type_params = _safe_get(params, "Type Parameters", {})
|
||||
inst_params = _safe_get(params, "Instance Parameters", {})
|
||||
|
||||
ifc_props = []
|
||||
|
||||
# Reference = TypeName (always present in Revit IFC)
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if type_name:
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# IsExternal — derive from builtInCategory or "Constraints" parameters
|
||||
bic = _safe_get(props, "builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if not is_external:
|
||||
# Some elements expose it directly as a parameter
|
||||
ext_val = _param_value(inst_params, "WALL_ATTR_ROOM_BOUNDING")
|
||||
if ext_val is not None:
|
||||
is_external = bool(ext_val)
|
||||
if ifc_class not in {"IfcSpace", "IfcSite", "IfcBuildingStorey", "IfcBuilding",
|
||||
"IfcFurnishingElement", "IfcOpeningElement"}:
|
||||
p = _make_prop(ifc, "IsExternal", "IfcBoolean", is_external)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# LoadBearing — walls, columns, beams, slabs
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcSlab", "IfcColumn", "IfcBeam"}:
|
||||
lb_val = (_param_value(inst_params, "WALL_STRUCTURAL_SIGNIFICANT") or
|
||||
_param_value(inst_params, "WALL_STRUCTURAL_USAGE_PARAM") or
|
||||
_param_value(type_params, "WALL_STRUCTURAL_SIGNIFICANT"))
|
||||
lb = bool(lb_val) if lb_val is not None else False
|
||||
p = _make_prop(ifc, "LoadBearing", "IfcBoolean", lb)
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
|
||||
# ThermalTransmittance — walls, roofs, slabs, doors, windows
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcRoof", "IfcSlab",
|
||||
"IfcDoor", "IfcWindow"}:
|
||||
u_val = (_param_value(type_params, "ANALYTICAL_HEAT_TRANSFER_COEFFICIENT") or
|
||||
_param_value(inst_params, "ANALYTICAL_HEAT_TRANSFER_COEFFICIENT"))
|
||||
if u_val is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "ThermalTransmittance", "IfcThermalTransmittanceMeasure", float(u_val))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# PitchAngle — roofs/slabs
|
||||
if ifc_class in {"IfcRoof", "IfcSlab"}:
|
||||
slope = _param_value(inst_params, "ROOF_SLOPE")
|
||||
if slope is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "PitchAngle", "IfcPlaneAngleMeasure", float(slope))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stair-specific
|
||||
if ifc_class in {"IfcStair", "IfcStairFlight"}:
|
||||
for internal, prop_name, factory in [
|
||||
("STAIR_RISER_HEIGHT", "RiserHeight", "IfcPositiveLengthMeasure"),
|
||||
("STAIR_TREAD_DEPTH", "TreadLength", "IfcPositiveLengthMeasure"),
|
||||
("STAIR_NUMBER_OF_RISERS","NumberOfRiser", "IfcCountMeasure"),
|
||||
("STAIR_NUMBER_OF_TREADS","NumberOfTreads", "IfcCountMeasure"),
|
||||
]:
|
||||
v = _param_value(inst_params, internal) or _param_value(type_params, internal)
|
||||
if v is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, prop_name, factory, float(v) if "Measure" in factory else int(v))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Railing height
|
||||
if ifc_class == "IfcRailing":
|
||||
h = _param_value(inst_params, "RAILING_HEIGHT") or _param_value(type_params, "RAILING_HEIGHT")
|
||||
if h is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "Height", "IfcPositiveLengthMeasure", float(h))
|
||||
if p:
|
||||
ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_write_pset(ifc, element, pset_name, ifc_props)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pset_EnvironmentalImpactIndicators (always written, Reference = TypeName)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_environmental_pset(ifc, element, obj: Base):
|
||||
"""Write Pset_EnvironmentalImpactIndicators with Reference = TypeName."""
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if not type_name:
|
||||
return
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
_write_pset(ifc, element, "Pset_EnvironmentalImpactIndicators", [p])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom Revit parameters pset (all remaining instance + type params)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _safe_str(value) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return "Yes" if value else "No"
|
||||
if isinstance(value, float):
|
||||
# Trim excessive decimals
|
||||
return f"{value:.6g}"
|
||||
if isinstance(value, (int, str)):
|
||||
s = str(value).strip()
|
||||
return s if s else None
|
||||
return str(value).strip() or None
|
||||
s = str(value).strip()
|
||||
return s or None
|
||||
|
||||
|
||||
def _extract_param(entry) -> tuple[str, str] | None:
|
||||
"""
|
||||
Given a Revit parameter entry dict like:
|
||||
{"name": "Thickness", "value": 25.4, "units": "Millimeters", ...}
|
||||
Returns (display_name, display_value) or None if unusable.
|
||||
"""
|
||||
if not isinstance(entry, dict):
|
||||
return None
|
||||
name = entry.get("name")
|
||||
value = entry.get("value")
|
||||
if not name or value is None:
|
||||
return None
|
||||
units = entry.get("units", "")
|
||||
# Skip non-informative unit labels
|
||||
def _flatten_params(params_block) -> dict:
|
||||
"""Flatten Type or Instance parameter block into {name: display_value}.
|
||||
Handles both plain dicts and Speckle Base objects at every nesting level."""
|
||||
result = {}
|
||||
skip_units = {"", "None", "General", "Currency", "Integer"}
|
||||
val_str = _safe_val(value)
|
||||
if val_str is None:
|
||||
return None
|
||||
if units and units not in skip_units:
|
||||
display = f"{val_str} {units}"
|
||||
else:
|
||||
display = val_str
|
||||
return str(name), display
|
||||
|
||||
|
||||
def _flatten_param_group(group: dict) -> dict:
|
||||
"""
|
||||
Flatten one parameter group (e.g. "Dimensions", "Constraints") dict.
|
||||
Each value is a Revit parameter entry {"name":..., "value":..., "units":...}.
|
||||
Returns {display_name: display_value}.
|
||||
"""
|
||||
result = {}
|
||||
if not isinstance(group, dict):
|
||||
return result
|
||||
for _internal_key, entry in group.items():
|
||||
pair = _extract_param(entry)
|
||||
if pair:
|
||||
name, val = pair
|
||||
result[name] = val
|
||||
return result
|
||||
|
||||
|
||||
def _extract_parameter_block(params_block: dict) -> dict:
|
||||
"""
|
||||
Flatten all groups in a parameter block (Type Parameters or Instance Parameters).
|
||||
Returns a merged {display_name: display_value} dict.
|
||||
"""
|
||||
result = {}
|
||||
if not isinstance(params_block, dict):
|
||||
return result
|
||||
for _group_name, group in params_block.items():
|
||||
result.update(_flatten_param_group(group))
|
||||
return result
|
||||
|
||||
|
||||
def _get_properties_dict(obj: Base) -> dict:
|
||||
"""Extract the raw properties dict from a DataObject."""
|
||||
for key in ["properties", "@properties", "_properties"]:
|
||||
try:
|
||||
props = obj[key]
|
||||
if props is None:
|
||||
continue
|
||||
if hasattr(props, "get_dynamic_member_names"):
|
||||
names = props.get_dynamic_member_names()
|
||||
return {n: props[n] for n in names}
|
||||
if isinstance(props, dict):
|
||||
return props
|
||||
except Exception:
|
||||
block = _to_dict(params_block)
|
||||
for group in block.values():
|
||||
group_d = _to_dict(group)
|
||||
if not group_d:
|
||||
continue
|
||||
return {}
|
||||
for entry in group_d.values():
|
||||
entry_d = _to_dict(entry)
|
||||
if not entry_d:
|
||||
continue
|
||||
name = entry_d.get("name")
|
||||
value = entry_d.get("value")
|
||||
units = entry_d.get("units", "") or ""
|
||||
if not name or value is None:
|
||||
continue
|
||||
val_str = _safe_str(value)
|
||||
if val_str is None:
|
||||
continue
|
||||
display = f"{val_str} {units}".strip() if units not in skip_units else val_str
|
||||
result[name] = display
|
||||
return result
|
||||
|
||||
|
||||
def _write_pset(ifc, element, pset_name: str, props: dict):
|
||||
"""Write a property set if there are any properties."""
|
||||
if not props:
|
||||
return
|
||||
try:
|
||||
pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name=pset_name)
|
||||
ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties=props)
|
||||
except Exception as e:
|
||||
print(f" ⚠️ {pset_name}: {e}")
|
||||
|
||||
|
||||
def write_properties(ifc, element, obj: Base):
|
||||
def write_revit_params(ifc, element, obj: Base):
|
||||
"""
|
||||
Write Revit parameters as IFC property sets.
|
||||
Creates separate psets for Type and Instance parameters.
|
||||
Write remaining Revit instance parameters as a custom property set
|
||||
using the vendor prefix 'RVT_' (not 'Pset_' which is reserved):
|
||||
RVT_InstanceParameters — from Instance Parameters
|
||||
|
||||
Note: RVT_TypeParameters are written on the IfcTypeObject (via TypeManager),
|
||||
not on individual elements, to avoid duplication.
|
||||
"""
|
||||
props_dict = _get_properties_dict(obj)
|
||||
parameters = props_dict.get("Parameters") or {}
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
|
||||
# Type Parameters → Pset_RevitTypeParameters
|
||||
type_params = parameters.get("Type Parameters") or {}
|
||||
type_flat = _extract_parameter_block(type_params)
|
||||
_write_pset(ifc, element, "RVT_TypeParameters", type_flat)
|
||||
inst_flat = _flatten_params(_safe_get(params, "Instance Parameters", {}))
|
||||
|
||||
# Instance Parameters → Pset_RevitInstanceParameters
|
||||
inst_params = parameters.get("Instance Parameters") or {}
|
||||
inst_flat = _extract_parameter_block(inst_params)
|
||||
_write_pset(ifc, element, "RVT_InstanceParameters", inst_flat)
|
||||
def build_str_props(flat: dict) -> list:
|
||||
out = []
|
||||
for name, val in flat.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
p = ifc.create_entity("IfcPropertySingleValue", Name=name, NominalValue=nominal)
|
||||
out.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
# Top-level semantic fields → Pset_RevitIdentity
|
||||
inst_props = build_str_props(inst_flat)
|
||||
|
||||
if inst_props:
|
||||
_write_pset(ifc, element, "RVT_InstanceParameters", inst_props)
|
||||
|
||||
# Identity: family, type, elementId, builtInCategory
|
||||
identity = {}
|
||||
for field in ["type", "family", "category", "level"]:
|
||||
for field in ["family", "type", "category"]:
|
||||
val = getattr(obj, field, None)
|
||||
if val and isinstance(val, str) and val.strip():
|
||||
identity[field.capitalize()] = val.strip()
|
||||
# Also include elementId if present
|
||||
elem_id = props_dict.get("elementId")
|
||||
elem_id = _safe_get(props, "elementId")
|
||||
if elem_id:
|
||||
identity["ElementId"] = str(elem_id)
|
||||
bic = _safe_get(props, "builtInCategory")
|
||||
if bic:
|
||||
identity["BuiltInCategory"] = str(bic)
|
||||
|
||||
_write_pset(ifc, element, "RVT_Identity", identity)
|
||||
id_props = []
|
||||
for name, val in identity.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
p = ifc.create_entity("IfcPropertySingleValue", Name=name, NominalValue=nominal)
|
||||
id_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
if id_props:
|
||||
_write_pset(ifc, element, "RVT_Identity", id_props)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — called from main.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def write_material_quantities(ifc, element, obj: Base):
|
||||
"""
|
||||
Write Material Quantities from Revit as IfcElementQuantity sets.
|
||||
|
||||
Source: properties."Material Quantities".<MaterialName>.{area, volume, density,
|
||||
materialName, materialClass, materialCategory}
|
||||
|
||||
Each material produces one IfcElementQuantity named "Qto_<MaterialName>" with:
|
||||
- GrossArea (IfcQuantityArea)
|
||||
- GrossVolume (IfcQuantityVolume)
|
||||
- Density (IfcPropertySingleValue — no standard IFC quantity type)
|
||||
- MaterialClass (IfcPropertySingleValue)
|
||||
- MaterialCategory (IfcPropertySingleValue)
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
mat_quantities = _safe_get(props, "Material Quantities")
|
||||
if mat_quantities is None:
|
||||
return
|
||||
|
||||
mat_dict = _to_dict(mat_quantities)
|
||||
if not mat_dict:
|
||||
return
|
||||
|
||||
for mat_key, mat_data in mat_dict.items():
|
||||
mat_d = _to_dict(mat_data)
|
||||
if not mat_d:
|
||||
continue
|
||||
|
||||
mat_name = mat_d.get("materialName") or mat_key
|
||||
quantities = []
|
||||
|
||||
# Area → IfcQuantityArea
|
||||
area_entry = _to_dict(mat_d.get("area"))
|
||||
if area_entry and area_entry.get("value") is not None:
|
||||
try:
|
||||
q = ifc.create_entity(
|
||||
"IfcQuantityArea",
|
||||
Name="GrossArea",
|
||||
AreaValue=float(area_entry["value"]),
|
||||
)
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Volume → IfcQuantityVolume
|
||||
vol_entry = _to_dict(mat_d.get("volume"))
|
||||
if vol_entry and vol_entry.get("value") is not None:
|
||||
try:
|
||||
q = ifc.create_entity(
|
||||
"IfcQuantityVolume",
|
||||
Name="GrossVolume",
|
||||
VolumeValue=float(vol_entry["value"]),
|
||||
)
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Density → IfcQuantityWeight (mass per volume, stored as weight)
|
||||
density_entry = _to_dict(mat_d.get("density"))
|
||||
if density_entry and density_entry.get("value") is not None:
|
||||
try:
|
||||
q = ifc.create_entity(
|
||||
"IfcQuantityWeight",
|
||||
Name="Density",
|
||||
WeightValue=float(density_entry["value"]),
|
||||
)
|
||||
quantities.append(q)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not quantities:
|
||||
continue
|
||||
|
||||
# Create IfcElementQuantity and link via IfcRelDefinesByProperties
|
||||
qto_name = f"Qto_{mat_name}"
|
||||
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:
|
||||
1. Pset_<Entity>Common — standard typed properties (Reference, IsExternal, etc.)
|
||||
2. Pset_EnvironmentalImpactIndicators — Reference = TypeName
|
||||
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)
|
||||
"""
|
||||
write_common_pset(ifc, element, obj, ifc_class, category_name)
|
||||
write_revit_params(ifc, element, obj)
|
||||
write_material_quantities(ifc, element, obj)
|
||||
|
||||
|
||||
def write_common_properties(ifc, element, obj: Base, category_name: str = ""):
|
||||
"""
|
||||
Write Pset_SpeckleData for traceability back to the Speckle source object.
|
||||
"""
|
||||
props = {}
|
||||
speckle_id = getattr(obj, "id", None)
|
||||
app_id = getattr(obj, "applicationId", None)
|
||||
speckle_type = getattr(obj, "speckle_type", None)
|
||||
"""Legacy shim — kept for compatibility with main.py call sites."""
|
||||
pass # All handled by write_properties now
|
||||
|
||||
if speckle_id: props["SpeckleId"] = str(speckle_id)
|
||||
if app_id: props["ApplicationId"] = str(app_id)
|
||||
if speckle_type: props["SpeckleType"] = str(speckle_type)
|
||||
if category_name: props["RevitCategory"] = str(category_name)
|
||||
|
||||
_write_pset(ifc, element, "RVT_SpeckleData", props)
|
||||
def reset_caches():
|
||||
"""Clear module-level caches (call at start of each export run)."""
|
||||
_props_cache.clear()
|
||||
_to_dict_cache.clear()
|
||||
@@ -0,0 +1,214 @@
|
||||
# =============================================================================
|
||||
# type_manager.py
|
||||
# Creates and caches IfcTypeObjects (IfcWallType, IfcRoofType, etc.) and
|
||||
# links element instances to them via IfcRelDefinesByType.
|
||||
#
|
||||
# Revit native IFC export pattern:
|
||||
# IfcWallType
|
||||
# Name = "Family:TypeName" (no ElementId)
|
||||
# Tag = Type's Revit ElementId (from Instance Parameters > Other > Type Id)
|
||||
# GlobalId = from Type IfcGUID param (from Type Parameters > IFC Parameters > Type IfcGUID)
|
||||
# HasPropertySets:
|
||||
# Pset_WallCommon: IsExternal, ThermalTransmittance (type-level)
|
||||
# Pset_EnvironmentalImpactIndicators: Reference = TypeName
|
||||
# RVT_TypeParameters: all remaining type params
|
||||
#
|
||||
# Type objects are SHARED — multiple instances of the same Revit type
|
||||
# map to one IfcTypeObject, keyed by (ifc_class, family, type_name).
|
||||
# =============================================================================
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api
|
||||
from specklepy.objects.base import Base
|
||||
from utils.properties import (
|
||||
_get_props_dict, _get_nested, _param_value, _make_prop, _write_pset,
|
||||
_safe_get, _to_dict,
|
||||
COMMON_PSET, EXTERNAL_CATEGORIES, _flatten_params
|
||||
)
|
||||
|
||||
|
||||
# IFC element class → IFC type class
|
||||
TYPE_CLASS_MAP: dict[str, str] = {
|
||||
"IfcWall": "IfcWallType",
|
||||
"IfcWallStandardCase": "IfcWallType",
|
||||
"IfcSlab": "IfcSlabType",
|
||||
"IfcRoof": "IfcRoofType",
|
||||
"IfcColumn": "IfcColumnType",
|
||||
"IfcBeam": "IfcBeamType",
|
||||
"IfcMember": "IfcMemberType",
|
||||
"IfcDoor": "IfcDoorType",
|
||||
"IfcWindow": "IfcWindowType",
|
||||
"IfcStair": "IfcStairType",
|
||||
"IfcStairFlight": "IfcStairFlightType",
|
||||
"IfcRamp": "IfcRampType",
|
||||
"IfcRailing": "IfcRailingType",
|
||||
"IfcCovering": "IfcCoveringType",
|
||||
"IfcCurtainWall": "IfcCurtainWallType",
|
||||
"IfcFooting": "IfcFootingType",
|
||||
"IfcBuildingElementProxy": "IfcBuildingElementProxyType",
|
||||
"IfcFurnishingElement": "IfcFurnitureType",
|
||||
"IfcLightFixture": "IfcLightFixtureType",
|
||||
"IfcElectricAppliance": "IfcElectricApplianceType",
|
||||
"IfcElectricDistributionBoard": "IfcElectricDistributionBoardType",
|
||||
"IfcSanitaryTerminal": "IfcSanitaryTerminalType",
|
||||
"IfcUnitaryEquipment": "IfcUnitaryEquipmentType",
|
||||
"IfcDuctSegment": "IfcDuctSegmentType",
|
||||
"IfcPipeSegment": "IfcPipeSegmentType",
|
||||
"IfcCableCarrierSegment": "IfcCableCarrierSegmentType",
|
||||
"IfcPlate": "IfcPlateType",
|
||||
}
|
||||
|
||||
|
||||
class TypeManager:
|
||||
"""
|
||||
Creates IfcTypeObjects on demand and caches them by (ifc_class, family, type_name).
|
||||
Call assign(element, obj, ifc_class) for each exported element.
|
||||
"""
|
||||
|
||||
def __init__(self, ifc: ifcopenshell.file):
|
||||
self._ifc = ifc
|
||||
# key: (ifc_class, family, type_name) → IfcTypeObject
|
||||
self._cache: dict[tuple, object] = {}
|
||||
# type_object → [element, ...] (for batched IfcRelDefinesByType)
|
||||
self._pending: dict[int, list] = {}
|
||||
|
||||
def assign(self, element, obj: Base, ifc_class: str):
|
||||
"""Create (or retrieve cached) type object and queue the assignment."""
|
||||
type_class = TYPE_CLASS_MAP.get(ifc_class)
|
||||
if not type_class:
|
||||
return
|
||||
|
||||
family = getattr(obj, "family", None) or ""
|
||||
type_name = getattr(obj, "type", None) or ""
|
||||
if not type_name:
|
||||
return
|
||||
|
||||
cache_key = (ifc_class, family, type_name)
|
||||
|
||||
if cache_key not in self._cache:
|
||||
type_obj = self._create_type(type_class, family, type_name, obj, ifc_class)
|
||||
self._cache[cache_key] = type_obj
|
||||
|
||||
type_obj = self._cache[cache_key]
|
||||
type_id = type_obj.id()
|
||||
|
||||
if type_id not in self._pending:
|
||||
self._pending[type_id] = []
|
||||
self._pending[type_id].append(element)
|
||||
|
||||
def flush(self):
|
||||
"""Write all IfcRelDefinesByType relationships."""
|
||||
for type_id, elements in self._pending.items():
|
||||
type_obj = self._ifc.by_id(type_id)
|
||||
ifcopenshell.api.run(
|
||||
"type.assign_type", self._ifc,
|
||||
related_objects=elements,
|
||||
relating_type=type_obj,
|
||||
)
|
||||
self._pending.clear()
|
||||
print(f" Type objects created: {len(self._cache)}")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
def _create_type(self, type_class: str, family: str, type_name: str,
|
||||
obj: Base, ifc_class: str):
|
||||
"""Instantiate the IfcTypeObject with name, tag, GlobalId, and psets."""
|
||||
props = _get_props_dict(obj)
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
type_params = _safe_get(params, "Type Parameters", {})
|
||||
inst_params = _safe_get(params, "Instance Parameters", {})
|
||||
|
||||
# Name: "Family:TypeName" (no ElementId)
|
||||
name_parts = [p for p in [family, type_name] if p]
|
||||
name = ":".join(name_parts)
|
||||
|
||||
# Tag: Type's Revit ElementId
|
||||
type_id_entry = _get_nested(inst_params, "Other", "Type Id")
|
||||
type_id_d = _to_dict(type_id_entry)
|
||||
tag = str(type_id_d.get("value")) if type_id_d.get("value") else None
|
||||
|
||||
# GlobalId: from Type IfcGUID parameter
|
||||
type_guid_entry = _get_nested(type_params, "IFC Parameters", "Type IfcGUID")
|
||||
type_guid_d = _to_dict(type_guid_entry)
|
||||
guid = type_guid_d.get("value") if type_guid_d else None
|
||||
|
||||
# Create type entity
|
||||
type_obj = ifcopenshell.api.run(
|
||||
"root.create_entity", self._ifc,
|
||||
ifc_class=type_class,
|
||||
name=name,
|
||||
)
|
||||
if tag:
|
||||
try:
|
||||
type_obj.Tag = str(tag)
|
||||
except Exception:
|
||||
pass
|
||||
if guid:
|
||||
try:
|
||||
type_obj.GlobalId = str(guid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Write type-level property sets
|
||||
self._write_type_psets(type_obj, obj, ifc_class, type_name, props,
|
||||
type_params, inst_params)
|
||||
return type_obj
|
||||
|
||||
def _write_type_psets(self, type_obj, obj, ifc_class, type_name,
|
||||
props, type_params, inst_params):
|
||||
"""Write psets on the type object (type-level parameters only)."""
|
||||
ifc = self._ifc
|
||||
pset_name = COMMON_PSET.get(ifc_class)
|
||||
|
||||
# ── Standard Common pset on the type ──────────────────────────────
|
||||
if pset_name:
|
||||
type_ifc_props = []
|
||||
|
||||
# IsExternal (type-level)
|
||||
bic = _safe_get(props, "builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if ifc_class not in {"IfcSpace", "IfcSite", "IfcBuildingStorey",
|
||||
"IfcBuilding", "IfcFurnishingElement", "IfcOpeningElement"}:
|
||||
p = _make_prop(ifc, "IsExternal", "IfcBoolean", is_external)
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
|
||||
# ThermalTransmittance (from type parameters)
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcRoof",
|
||||
"IfcSlab", "IfcDoor", "IfcWindow"}:
|
||||
u_val = _param_value(type_params,
|
||||
"ANALYTICAL_HEAT_TRANSFER_COEFFICIENT")
|
||||
if u_val is not None:
|
||||
try:
|
||||
p = _make_prop(ifc, "ThermalTransmittance",
|
||||
"IfcThermalTransmittanceMeasure", float(u_val))
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# LoadBearing (from type parameters)
|
||||
if ifc_class in {"IfcWall", "IfcWallStandardCase", "IfcColumn",
|
||||
"IfcBeam", "IfcSlab"}:
|
||||
lb_val = _param_value(type_params, "WALL_STRUCTURAL_SIGNIFICANT")
|
||||
if lb_val is not None:
|
||||
p = _make_prop(ifc, "LoadBearing", "IfcBoolean", bool(lb_val))
|
||||
if p:
|
||||
type_ifc_props.append(p)
|
||||
|
||||
if type_ifc_props:
|
||||
_write_pset(ifc, type_obj, pset_name, type_ifc_props)
|
||||
|
||||
# ── RVT_TypeParameters — all type-level Revit params ──────────────
|
||||
type_flat = _flatten_params(type_params)
|
||||
if type_flat:
|
||||
type_str_props = []
|
||||
for name_p, val in type_flat.items():
|
||||
try:
|
||||
nominal = ifc.create_entity("IfcLabel", wrappedValue=val)
|
||||
prop = ifc.create_entity("IfcPropertySingleValue",
|
||||
Name=name_p, NominalValue=nominal)
|
||||
type_str_props.append(prop)
|
||||
except Exception:
|
||||
pass
|
||||
if type_str_props:
|
||||
_write_pset(ifc, type_obj, "RVT_TypeParameters", type_str_props)
|
||||
+2
-2
@@ -74,7 +74,7 @@ def create_ifc_scaffold() -> tuple:
|
||||
products=[building],
|
||||
)
|
||||
|
||||
return ifc, building, body_ctx
|
||||
return ifc, site, building, body_ctx
|
||||
|
||||
|
||||
class StoreyManager:
|
||||
@@ -112,4 +112,4 @@ class StoreyManager:
|
||||
|
||||
@property
|
||||
def names(self) -> list[str]:
|
||||
return list(self._storeys.keys())
|
||||
return list(self._storeys.keys())
|
||||
Reference in New Issue
Block a user