ifcpatch: fix MergeProjects context deduplication and remove stale stub

Contexts were deleted with remove_deep2 into a single set, but
file.add() inflates inverse counts causing phantom references. Split
into coord_ops (0 real inverses, remove_deep2 recurses to clean
IfcProjectedCRS) and contexts (sorted parent-first so phantoms don't
become dangling references). Remove stale MakeVolume stub from
ifcopenshell_wrapper.pyi.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-04-09 01:14:30 +01:00
parent b4558f7f75
commit 039c848d33
2 changed files with 23 additions and 11 deletions
@@ -390,12 +390,6 @@ class JsonSerializer:
def setFile(self, arg2): ...
def writeHeader(self): ...
# TODO: MakeVolume is ignored in SWIG, remove from stub once build is bumped.
class MakeVolume:
defaultvalue: Any
description: Any
name: Any
class OpaqueCoordinate_3:
def __init__(self, *args): ...
def get(self, i): ...
+23 -5
View File
@@ -158,7 +158,8 @@ class Patcher(ifcpatch.BasePatcher):
return ifcopenshell.util.unit.get_full_unit_name(length_unit)
def reuse_existing_contexts(self) -> None:
to_delete = set()
contexts_to_delete: set[int] = set()
coord_ops_to_delete: set[int] = set()
for added_context in self.added_contexts:
equivalent_existing_context = self.get_equivalent_existing_context(added_context)
@@ -166,15 +167,32 @@ class Patcher(ifcpatch.BasePatcher):
for inverse in self.file.get_inverse(added_context):
if self.file.schema != "IFC2X3":
if inverse.is_a("IfcCoordinateOperation"):
to_delete.add(inverse.id())
coord_ops_to_delete.add(inverse.id())
continue
ifcopenshell.util.element.replace_attribute(inverse, added_context, equivalent_existing_context)
to_delete.add(added_context.id())
contexts_to_delete.add(added_context.id())
for element_id in to_delete:
# IfcCoordinateOperation entities (e.g. IfcMapConversion) have 0 real inverses,
# so remove_deep2 works and also cleans up owned sub-entities (e.g. IfcProjectedCRS).
for element_id in coord_ops_to_delete:
try:
ifcopenshell.util.element.remove_deep2(self.file, self.file.by_id(element_id))
except:
except Exception:
pass
# Delete parent contexts before subcontexts: file.add() inflates inverse counts,
# leaving a phantom subcontext entry in the parent's index. Deleting the subcontext
# first turns it into a dangling reference; deleting the parent first is safe.
def deletion_priority(element_id: int) -> int:
try:
return 1 if self.file.by_id(element_id).is_a("IfcGeometricRepresentationSubContext") else 0
except Exception:
return 2
for element_id in sorted(contexts_to_delete, key=deletion_priority):
try:
self.file.remove(self.file.by_id(element_id))
except Exception:
pass
def get_equivalent_existing_context(