ifc-commit/docs/merge.md
2026-03-25 10:36:30 +01:00

3.9 KiB
Raw Permalink Blame History

IFC Merging

How IFC files are merged in the ifc-commit pipeline.


Recipe: MergeProjects

All merging in ifccommit uses the ifcpatch recipe MergeProjects. It combines two or more ifcopenshell.file objects into a single model:

import ifcopenshell
import ifcpatch

base  = ifcopenshell.open("base.ifc")
part  = ifcopenshell.open("part.ifc")

result = ifcpatch.execute({
    "input": "base.ifc",
    "file":  base,
    "recipe": "MergeProjects",
    "arguments": [[part]],   # list of ifcopenshell.file objects
})

ifcpatch.write(result, "merged.ifc")

Important: always pass ifcopenshell.file objects, not file paths. Passing a path string causes a segfault in the current ifcopenshell version.


What MergeProjects does

  1. Combines IfcProject elements — the two projects are merged into one.
  2. Appends all entities from the part model into the base model, remapping entity ids to avoid collisions.
  3. Converts length units — the part is automatically rescaled to match the base model's unit before merging.
  4. Does not deduplicate spatial hierarchies — if both files have a site, building, and storey, the result will contain two of each. Use replace instead of insert when updating an existing space.

insert vs replace

Command Behaviour
insert Appends part into base — no removal. May duplicate spatial elements.
replace Removes the target space first, then merges part in. Clean result.

insert

base.ifc  (295 products)
+ part.ifc  (9 products)
= merged.ifc  (304 products)   ← space objects duplicated

Used when the part introduces new elements that do not already exist in the base.

replace

base.ifc  (295 products)
 space A102 + contents  (6 elements removed)
+ modified_space.ifc  (9 products)
= merged.ifc  (298 products)   ← clean replacement

Used when updating a space that already exists in the base model.


Internals of replace

cmd_replace in ifccommit.py performs the removal step manually before merging:

# 1. Find and remove the space and its contained elements
contained = ifcopenshell.util.selector.filter_elements(
    model, f'IfcElement, location = "{space_name}"'
)
for el in list(contained) + [space]:
    ifcopenshell.api.run("root.remove_product", model, product=el)

# 2. Write stripped base to a temp file
model.write(tmp)

# 3. Merge the part in
stripped   = ifcopenshell.open(tmp)
part_model = ifcopenshell.open(part_path)
result = ifcpatch.execute({
    "input": tmp, "file": stripped,
    "recipe": "MergeProjects",
    "arguments": [[part_model]],
})
ifcpatch.write(result, output)

root.remove_product cleans up the element and its direct relationships (placements, containment links). Geometry and shared assets are handled by the model's internal garbage collection on write.


Multi-type merging in split

The location filter in ifcopenshell's selector breaks when combined with the + operator for multiple types (IfcWall + IfcSlab, location = "Level 1" silently drops the filter). The workaround used in cmd_split:

  1. Extract each type separately with its own location-filtered query.
  2. Write each extraction to a temp file.
  3. Merge all temp files with MergeProjects.
  4. Clean up temp files.

This is encapsulated in the extract_with_location() helper in ifccommit.py.


Known quirks

  • Segfault with path stringsMergeProjects crashes if given a file path instead of an ifcopenshell.file object. Always open the file first.
  • Duplicate spatial hierarchyinsert does not merge sites, buildings, or storeys; use replace for clean substitution.
  • Aggregated vs contained spaces — the location filter only follows IfcRelContainedInSpatialStructure, not IfcRelAggregates. Spaces linked via aggregation appear empty to the filter (e.g. space B104 in duplex.ifc).