Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8c9d4237d | |||
| 11acb02fd1 | |||
| f7aa6c29da | |||
| 63082a881c |
@@ -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,16 +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, build_element_name, get_element_tag, get_ifc_guid
|
||||
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",
|
||||
}
|
||||
@@ -60,6 +60,10 @@ def automate_function(
|
||||
print(" Speckle -> IFC4.3 Exporter")
|
||||
print("=" * 60)
|
||||
|
||||
# Reset caches from any previous run
|
||||
reset_props_caches()
|
||||
reset_mapper_caches()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Receive
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -78,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)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -113,12 +117,20 @@ def automate_function(
|
||||
# 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)
|
||||
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))
|
||||
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
|
||||
@@ -135,7 +147,9 @@ def automate_function(
|
||||
rep, placement = mesh_to_ifc(ifc, body_context, obj, scale=scale, material_manager=material_manager)
|
||||
if rep:
|
||||
element = _create_element(ifc, ifc_class, name, rep, placement, storey,
|
||||
tag=get_element_tag(obj), guid=get_ifc_guid(obj))
|
||||
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
|
||||
@@ -152,7 +166,10 @@ def automate_function(
|
||||
no_geometry += 1
|
||||
continue
|
||||
inst_element = _create_element(
|
||||
ifc, ifc_class, name, inst_rep, inst_placement, storey
|
||||
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)
|
||||
@@ -178,7 +195,13 @@ def automate_function(
|
||||
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!")
|
||||
@@ -191,15 +214,23 @@ def automate_function(
|
||||
print_instance_stats()
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def _create_element(ifc, ifc_class, name, rep, placement, storey, tag=None, guid=None):
|
||||
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:
|
||||
element.Tag = str(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
|
||||
@@ -215,11 +246,20 @@ def _create_element(ifc, ifc_class, name, rep, placement, storey, tag=None, guid
|
||||
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
+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)
|
||||
|
||||
|
||||
+227
-98
@@ -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")
|
||||
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
|
||||
|
||||
+74
-14
@@ -36,6 +36,7 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
# Architectural - Stairs / Ramps / Railings
|
||||
"OST_Stairs": "IfcStair",
|
||||
"OST_StairsRailing": "IfcRailing",
|
||||
"OST_RailingTopRail": "IfcRailing",
|
||||
"OST_Ramps": "IfcRamp",
|
||||
"OST_StairsLandings": "IfcStairFlight",
|
||||
"OST_StairsRuns": "IfcStairFlight",
|
||||
@@ -101,6 +102,7 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
# Site / Civil
|
||||
"OST_Site": "IfcSite",
|
||||
"OST_Topography": "IfcGeographicElement",
|
||||
"OST_Toposolid": "IfcGeographicElement",
|
||||
"OST_Roads": "IfcRoad",
|
||||
"OST_Hardscape": "IfcPavement",
|
||||
"OST_Planting": "IfcGeographicElement",
|
||||
@@ -117,6 +119,12 @@ BUILTIN_CATEGORY_MAP: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
# --- 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",
|
||||
@@ -173,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",
|
||||
@@ -188,6 +197,8 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
"Structural Foundations": "IfcFooting",
|
||||
"Foundation Slabs": "IfcSlab",
|
||||
"Topography": "IfcGeographicElement",
|
||||
"Toposolid": "IfcGeographicElement",
|
||||
"Planting": "IfcGeographicElement",
|
||||
"Site": "IfcSite",
|
||||
"Parking": "IfcSpace",
|
||||
"Generic Models": "IfcBuildingElementProxy",
|
||||
@@ -196,24 +207,55 @@ CATEGORY_MAP: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
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 = obj["properties"] or getattr(obj, "properties", None)
|
||||
props = getattr(obj, "properties", None)
|
||||
if props is None:
|
||||
return None
|
||||
if hasattr(props, "__getitem__"):
|
||||
val = props["builtInCategory"]
|
||||
else:
|
||||
try:
|
||||
props = obj["properties"]
|
||||
except Exception:
|
||||
pass
|
||||
if props is not None:
|
||||
val = getattr(props, "builtInCategory", None)
|
||||
if val and isinstance(val, str):
|
||||
return val.strip()
|
||||
if val is None:
|
||||
try:
|
||||
val = props["builtInCategory"]
|
||||
except Exception:
|
||||
pass
|
||||
if val and isinstance(val, str):
|
||||
result = val.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
_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:
|
||||
@@ -227,6 +269,16 @@ def classify(obj, category_name: str = "") -> str:
|
||||
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:
|
||||
@@ -244,8 +296,9 @@ def classify(obj, category_name: str = "") -> str:
|
||||
if category_name:
|
||||
if category_name in CATEGORY_MAP:
|
||||
return CATEGORY_MAP[category_name]
|
||||
for key, ifc_class in CATEGORY_MAP.items():
|
||||
if key.lower() in category_name.lower():
|
||||
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
|
||||
@@ -253,8 +306,15 @@ def classify(obj, category_name: str = "") -> str:
|
||||
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():
|
||||
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"
|
||||
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
|
||||
|
||||
+228
-61
@@ -52,6 +52,7 @@ COMMON_PSET: dict[str, str] = {
|
||||
"IfcLightFixture": "Pset_LightFixtureTypeCommon",
|
||||
"IfcOpeningElement": "Pset_OpeningElementCommon",
|
||||
"IfcPlate": "Pset_PlateCommon",
|
||||
"IfcGeographicElement": "Pset_SiteCommon",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -129,50 +130,124 @@ EXTERNAL_CATEGORIES = {
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_props_cache: dict[int, dict] = {} # id(obj) → props dict
|
||||
|
||||
|
||||
def _get_props_dict(obj: Base) -> dict:
|
||||
for key in ["properties", "@properties"]:
|
||||
try:
|
||||
p = obj[key]
|
||||
if p is None:
|
||||
"""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 hasattr(p, "get_dynamic_member_names"):
|
||||
return {n: p[n] for n in p.get_dynamic_member_names()}
|
||||
if isinstance(p, dict):
|
||||
return p
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
if p is None:
|
||||
_props_cache[oid] = {}
|
||||
return {}
|
||||
result = _to_dict(p)
|
||||
_props_cache[oid] = result
|
||||
return result
|
||||
|
||||
|
||||
def _get_nested(d: dict, *keys):
|
||||
def _get_nested(d, *keys):
|
||||
"""Safely walk nested dicts/objects."""
|
||||
cur = d
|
||||
for k in keys:
|
||||
if cur is None:
|
||||
return None
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(k)
|
||||
else:
|
||||
try:
|
||||
cur = cur[k]
|
||||
except Exception:
|
||||
return None
|
||||
cur = _safe_get(cur, k)
|
||||
return cur
|
||||
|
||||
|
||||
def _param_value(params_block: dict, internal_name: str):
|
||||
_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.
|
||||
"""
|
||||
if not isinstance(params_block, dict):
|
||||
block = _to_dict(params_block)
|
||||
if not block:
|
||||
return None
|
||||
for group in params_block.values():
|
||||
if not isinstance(group, dict):
|
||||
for group in block.values():
|
||||
group_d = _to_dict(group)
|
||||
if not group_d:
|
||||
continue
|
||||
for entry in group.values():
|
||||
if isinstance(entry, dict) and entry.get("internalDefinitionName") == internal_name:
|
||||
return entry.get("value")
|
||||
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
|
||||
|
||||
|
||||
@@ -210,10 +285,8 @@ 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.
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
family = getattr(obj, "family", None) or ""
|
||||
typ = getattr(obj, "type", None) or ""
|
||||
elem_id = props.get("elementId", "") or getattr(obj, "applicationId", "") or ""
|
||||
|
||||
# Treat literal "none" (case-insensitive) the same as empty — Revit exports
|
||||
# placeholder objects with family/type set to the string "none".
|
||||
@@ -223,15 +296,13 @@ def build_element_name(obj: Base) -> str:
|
||||
typ = ""
|
||||
|
||||
parts = [p for p in [family, typ] if p]
|
||||
if elem_id:
|
||||
parts.append(str(elem_id))
|
||||
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 = props.get("elementId")
|
||||
elem_id = _safe_get(props, "elementId")
|
||||
return str(elem_id) if elem_id else None
|
||||
|
||||
|
||||
@@ -241,11 +312,12 @@ def get_ifc_guid(obj: Base) -> str | None:
|
||||
Falls back to None (ifcopenshell will auto-generate a GUID).
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
inst = params.get("Instance Parameters") or {}
|
||||
ifc_p = inst.get("IFC Parameters") or {}
|
||||
entry = ifc_p.get("IfcGUID") or {}
|
||||
val = entry.get("value") if isinstance(entry, dict) else None
|
||||
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
|
||||
|
||||
|
||||
@@ -263,9 +335,9 @@ def write_common_pset(ifc, element, obj: Base, ifc_class: str, category_name: st
|
||||
return
|
||||
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
type_params = params.get("Type Parameters") or {}
|
||||
inst_params = params.get("Instance Parameters") or {}
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
type_params = _safe_get(params, "Type Parameters", {})
|
||||
inst_params = _safe_get(params, "Instance Parameters", {})
|
||||
|
||||
ifc_props = []
|
||||
|
||||
@@ -277,7 +349,7 @@ def write_common_pset(ifc, element, obj: Base, ifc_class: str, category_name: st
|
||||
ifc_props.append(p)
|
||||
|
||||
# IsExternal — derive from builtInCategory or "Constraints" parameters
|
||||
bic = props.get("builtInCategory", "")
|
||||
bic = _safe_get(props, "builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if not is_external:
|
||||
# Some elements expose it directly as a parameter
|
||||
@@ -384,19 +456,23 @@ def _safe_str(value) -> str | None:
|
||||
return s or None
|
||||
|
||||
|
||||
def _flatten_params(params_block: dict) -> dict:
|
||||
"""Flatten Type or Instance parameter block into {name: display_value}."""
|
||||
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"}
|
||||
for group in params_block.values():
|
||||
if not isinstance(group, dict):
|
||||
block = _to_dict(params_block)
|
||||
for group in block.values():
|
||||
group_d = _to_dict(group)
|
||||
if not group_d:
|
||||
continue
|
||||
for entry in group.values():
|
||||
if not isinstance(entry, dict):
|
||||
for entry in group_d.values():
|
||||
entry_d = _to_dict(entry)
|
||||
if not entry_d:
|
||||
continue
|
||||
name = entry.get("name")
|
||||
value = entry.get("value")
|
||||
units = entry.get("units", "") or ""
|
||||
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)
|
||||
@@ -409,16 +485,17 @@ def _flatten_params(params_block: dict) -> dict:
|
||||
|
||||
def write_revit_params(ifc, element, obj: Base):
|
||||
"""
|
||||
Write remaining Revit parameters as two custom property sets
|
||||
Write remaining Revit instance parameters as a custom property set
|
||||
using the vendor prefix 'RVT_' (not 'Pset_' which is reserved):
|
||||
RVT_TypeParameters — from Type Parameters
|
||||
RVT_InstanceParameters — from Instance Parameters
|
||||
|
||||
Note: RVT_TypeParameters are written on the IfcTypeObject (via TypeManager),
|
||||
not on individual elements, to avoid duplication.
|
||||
"""
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
params = _safe_get(props, "Parameters", {})
|
||||
|
||||
type_flat = _flatten_params(params.get("Type Parameters") or {})
|
||||
inst_flat = _flatten_params(params.get("Instance Parameters") or {})
|
||||
inst_flat = _flatten_params(_safe_get(params, "Instance Parameters", {}))
|
||||
|
||||
def build_str_props(flat: dict) -> list:
|
||||
out = []
|
||||
@@ -431,11 +508,8 @@ def write_revit_params(ifc, element, obj: Base):
|
||||
pass
|
||||
return out
|
||||
|
||||
type_props = build_str_props(type_flat)
|
||||
inst_props = build_str_props(inst_flat)
|
||||
|
||||
if type_props:
|
||||
_write_pset(ifc, element, "RVT_TypeParameters", type_props)
|
||||
if inst_props:
|
||||
_write_pset(ifc, element, "RVT_InstanceParameters", inst_props)
|
||||
|
||||
@@ -445,10 +519,10 @@ def write_revit_params(ifc, element, obj: Base):
|
||||
val = getattr(obj, field, None)
|
||||
if val and isinstance(val, str) and val.strip():
|
||||
identity[field.capitalize()] = val.strip()
|
||||
elem_id = props.get("elementId")
|
||||
elem_id = _safe_get(props, "elementId")
|
||||
if elem_id:
|
||||
identity["ElementId"] = str(elem_id)
|
||||
bic = props.get("builtInCategory")
|
||||
bic = _safe_get(props, "builtInCategory")
|
||||
if bic:
|
||||
identity["BuiltInCategory"] = str(bic)
|
||||
|
||||
@@ -468,6 +542,92 @@ def write_revit_params(ifc, element, obj: Base):
|
||||
# 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:
|
||||
@@ -476,12 +636,19 @@ def write_properties(ifc, element, obj: Base, ifc_class: str = "", category_name
|
||||
3. RVT_TypeParameters — all remaining Revit type parameters
|
||||
4. RVT_InstanceParameters — all remaining Revit instance parameters
|
||||
5. RVT_Identity — family, type, elementId, builtInCategory
|
||||
6. Qto_<MaterialName> — material quantities (area, volume, density)
|
||||
"""
|
||||
write_common_pset(ifc, element, obj, ifc_class, category_name)
|
||||
write_environmental_pset(ifc, element, obj)
|
||||
write_revit_params(ifc, element, obj)
|
||||
write_material_quantities(ifc, element, obj)
|
||||
|
||||
|
||||
def write_common_properties(ifc, element, obj: Base, category_name: str = ""):
|
||||
"""Legacy shim — kept for compatibility with main.py call sites."""
|
||||
pass # All handled by write_properties now
|
||||
pass # All handled by write_properties now
|
||||
|
||||
|
||||
def reset_caches():
|
||||
"""Clear module-level caches (call at start of each export run)."""
|
||||
_props_cache.clear()
|
||||
_to_dict_cache.clear()
|
||||
+9
-14
@@ -22,6 +22,7 @@ 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
|
||||
)
|
||||
|
||||
@@ -112,9 +113,9 @@ class TypeManager:
|
||||
obj: Base, ifc_class: str):
|
||||
"""Instantiate the IfcTypeObject with name, tag, GlobalId, and psets."""
|
||||
props = _get_props_dict(obj)
|
||||
params = props.get("Parameters") or {}
|
||||
type_params = params.get("Type Parameters") or {}
|
||||
inst_params = params.get("Instance Parameters") or {}
|
||||
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]
|
||||
@@ -122,13 +123,13 @@ class TypeManager:
|
||||
|
||||
# Tag: Type's Revit ElementId
|
||||
type_id_entry = _get_nested(inst_params, "Other", "Type Id")
|
||||
tag = str(type_id_entry.get("value")) if isinstance(type_id_entry, dict) else None
|
||||
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")
|
||||
guid = None
|
||||
if isinstance(type_guid_entry, dict):
|
||||
guid = type_guid_entry.get("value")
|
||||
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(
|
||||
@@ -163,7 +164,7 @@ class TypeManager:
|
||||
type_ifc_props = []
|
||||
|
||||
# IsExternal (type-level)
|
||||
bic = props.get("builtInCategory", "")
|
||||
bic = _safe_get(props, "builtInCategory", "")
|
||||
is_external = bic in EXTERNAL_CATEGORIES
|
||||
if ifc_class not in {"IfcSpace", "IfcSite", "IfcBuildingStorey",
|
||||
"IfcBuilding", "IfcFurnishingElement", "IfcOpeningElement"}:
|
||||
@@ -197,12 +198,6 @@ class TypeManager:
|
||||
if type_ifc_props:
|
||||
_write_pset(ifc, type_obj, pset_name, type_ifc_props)
|
||||
|
||||
# ── Pset_EnvironmentalImpactIndicators on the type ─────────────────
|
||||
if type_name:
|
||||
p = _make_prop(ifc, "Reference", "IfcIdentifier", type_name)
|
||||
if p:
|
||||
_write_pset(ifc, type_obj, "Pset_EnvironmentalImpactIndicators", [p])
|
||||
|
||||
# ── RVT_TypeParameters — all type-level Revit params ──────────────
|
||||
type_flat = _flatten_params(type_params)
|
||||
if type_flat:
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ def create_ifc_scaffold() -> tuple:
|
||||
products=[building],
|
||||
)
|
||||
|
||||
return ifc, building, body_ctx
|
||||
return ifc, site, building, body_ctx
|
||||
|
||||
|
||||
class StoreyManager:
|
||||
|
||||
Reference in New Issue
Block a user