# 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: ```python 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: ```python # 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 strings** — `MergeProjects` crashes if given a file path instead of an `ifcopenshell.file` object. Always open the file first. - **Duplicate spatial hierarchy** — `insert` 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).