# CLAUDE.md This file provides guidance to Claude Code when working in this repository of IFC building model files. ## Overview This repository contains IFC (Industry Foundation Classes) building models. IFC is an open standard (ISO 16739) for sharing building and infrastructure data. The repository uses standard tooling for: - **Querying and editing** models with `ifcquery` / `ifcedit` / `ifcmcp` - **Validation** against schema rules and IDS specifications - **Continuous integration** — models are validated automatically on every commit - **Branch-based workflows** — feature branches for model changes, `ifcmerge` for conflict resolution ## Preferred Tooling ### ifcmcp (preferred for interactive sessions) The `ifcmcp` MCP server holds the IFC model in memory across tool calls — no file I/O between operations. It is always the first choice for multi-step queries and edits. **Setup** (add to `.mcp.json`): ```json { "mcpServers": { "ifc": { "type": "stdio", "command": "python3", "args": ["-m", "ifcmcp"] } } } ``` Or via CLI: `claude mcp add --transport stdio ifc -- python3 -m ifcmcp` ### ifcquery / ifcedit (CLI — useful for scripting) ```bash python3 -m ifcquery [args] python3 -m ifcedit [args] ``` Each invocation reads/writes from disk. Use for scripting or when MCP is not available. --- ## MCP Tool Reference ### Session tools - **`ifc_load(path)`** — Open an IFC file into memory. Must be called before any other tool. - **`ifc_save(path="")`** — Write model to disk. Empty path overwrites the original file. - **`ifc_reset()`** — Clear in-memory model without saving. ### Query tools (read-only) - **`ifc_summary()`** — Schema version, entity counts, project name/description. - **`ifc_tree()`** — Full spatial hierarchy: Project → Site → Building → Storeys → Spaces → Elements. - **`ifc_info(element_id)`** — Deep inspection of any entity by step ID: attributes, property sets, resolved 4×4 placement matrix, type, material, container. - **`ifc_select(query)`** — Filter elements by IFC class (e.g. `"IfcWall"`, `"IfcWindow"`). Returns `[{id, type, name}]`. - **`ifc_relations(element_id, traverse="")`** — All relationships for an element. Use `traverse="up"` to walk the hierarchy up to IfcProject. - **`ifc_clash(element_id, clearance=0, tolerance=0.002, scope="storey")`** — Geometric clash detection within the same storey (or wider scope). - **`ifc_validate(express_rules=False)`** — Schema and constraint validation. Pass `express_rules=True` for full EXPRESS rule checking (slower). Returns `{"valid": bool, "issues": [...]}`. - **`ifc_schedule(max_depth=None)`** — Work schedules with nested task trees and start/finish dates. - **`ifc_cost(max_depth=None)`** — Cost schedules with nested cost item trees. - **`ifc_schema(entity_type)`** — IFC class documentation for the loaded model's schema version. ### Edit discovery tools - **`ifc_list(module="")`** — List all API modules, or functions within a module (e.g. `"sequence"`, `"root"`, `"geometry"`). - **`ifc_docs(function_path)`** — Full parameter documentation for an API function (e.g. `"root.remove_product"`, `"sequence.add_task"`). ### Edit execution - **`ifc_edit(function_path, params="{}")`** — Execute an `ifcopenshell.api` mutation. `params` is a JSON string. Returns `{"ok": true, "result": ...}` or `{"ok": false, "error": "..."}`. Does **not** auto-save. - **`ifc_quantify(rule, selector="")`** — Run quantity take-off. Writes `IfcElementQuantity` psets to the in-memory model. Does **not** auto-save. Rules: `"IFC4QtoBaseQuantities"` or `"IFC4X3QtoBaseQuantities"`. ### Parameter coercion in `ifc_edit` | Type | JSON value | Python result | |---|---|---| | `entity_instance` | `"42"` | Entity resolved from model by step ID | | `list[entity_instance]` | `"5,6,7"` | List of resolved entities | | `dict` | `'{"key": "val"}'` | Parsed JSON object | | `bool` | `"true"` | `True` | | `Optional[X]` | `"none"` | `None` | --- ## Standard Workflow ``` ifc_load("model.ifc") ifc_summary() # understand what's in the model ifc_tree() # see spatial hierarchy ifc_validate(express_rules=True) # check model health before editing # Query elements of interest ifc_select("IfcWall") ifc_info() ifc_relations() # Discover available edits ifc_list("sequence") ifc_docs("sequence.add_task") # Edit (multiple calls, all in-memory) ifc_edit("...", '{"param": "value"}') # Verify, then save ifc_validate() ifc_save() ``` --- ## IFC Concepts ### Spatial Hierarchy ``` IfcProject └─ IfcSite └─ IfcBuilding ├─ IfcBuildingStorey "Ground Floor" │ ├─ IfcSpace "kitchen" (aggregated via IfcRelAggregates) │ │ ├─ IfcWindow (contained in space via IfcRelContainedInSpatialStructure) │ │ └─ IfcDoor │ ├─ IfcWall (contained directly in storey via IfcRelContainedInSpatialStructure) │ │ └─ IfcOpeningElement (via IfcRelVoidsElement) │ │ └─ IfcWindow (via IfcRelFillsElement) │ ├─ IfcElementAssembly (optional grouping of walls) │ │ └─ IfcWall (parts, via IfcRelAggregates) │ ├─ IfcSlab, IfcFooting, ... └─ IfcBuildingStorey "First Floor" └─ ... ``` **Important notes on containment:** - Elements are contained in a spatial element (project, site, building, storey, or space) via `IfcRelContainedInSpatialStructure` — windows and doors are not necessarily in a space; they may be contained directly in a storey or building - Walls may appear directly in a storey (contained), or as parts of an `IfcElementAssembly` (aggregated) — both patterns are valid - Windows/doors appear in two places simultaneously: spatially contained in a spatial element, and structurally filling an opening in a wall (via `IfcRelFillsElement`). Both relationships must be maintained when editing. ### Key Relationships | Relationship | Meaning | |---|---| | `IfcRelAggregates` | Whole → parts (project → site, assembly → walls) | | `IfcRelContainedInSpatialStructure` | Element belongs to a spatial container | | `IfcRelVoidsElement` | Opening cuts through a wall/slab | | `IfcRelFillsElement` | Window/door fills an opening | | `IfcRelSpaceBoundary` | Element bounds a space (thermal/acoustic boundary) | | `IfcRelDefinesByType` | Element instance → type | | `IfcRelDefinesByProperties` | Element → property set | | `IfcRelAssignsToProcess` | Element → construction task | | `IfcRelSequence` | Task A finishes before task B starts | --- ## Common Editing Recipes ### Deleting a window or door Windows and doors have multiple relationships that must all be cleaned up: 1. **Find the opening**: `ifc_relations()` → note `filled_void` → IfcOpeningElement ID 2. **Find space boundaries**: `ifc_relations()` → note any `IfcRelSpaceBoundary` IDs referencing this element 3. **Delete the window**: `ifc_edit("root.remove_product", '{"product": ""}')` 4. **Delete the orphaned opening**: `ifc_edit("root.remove_product", '{"product": ""}')` `root.remove_product` is a smart delete — removes the entity plus all its relationships (geometry, placement, properties, materials, containment, type assignments, space boundaries). The IfcOpeningElement must be removed separately. **Verify no orphaned openings remain:** ``` ifc_relations() # → check children.openings for unfilled IfcOpeningElements ``` ### Moving a window or door ``` ifc_edit("geometry.edit_object_placement", '{"product": "", "matrix": [[...],[...],[...],[0,0,0,1]]}') ifc_edit("geometry.edit_object_placement", '{"product": "", "matrix": [[...],[...],[...],[0,0,0,1]]}') ``` Always move the IfcOpeningElement to the same position as the window/door. Also check `IfcRelSpaceBoundary` entities — their geometry may need updating if the window moves between spaces or changes boundary. ### Changing element types ``` ifc_select("IfcWindowType") # list available types ifc_edit("type.assign_type", '{"related_objects": "", "relating_type": ""}') ``` Type assignment also remaps geometry representations from the type to the occurrence. ### Editing attributes on any entity `attribute.edit_attributes` works on any entity, not just rooted products — useful for geometry entities, placements, task times, etc.: ``` ifc_edit("attribute.edit_attributes", '{"product": "", "attributes": "{\"Depth\": 3.0}"}') ``` Note the nested JSON: `params` is a JSON string and `attributes` within it is also a JSON string. ### Resizing element geometry `attribute.edit_attributes` on `IfcCartesianPointList2D.CoordList` fails with a type error (expects `AGGREGATE OF AGGREGATE OF DOUBLE`, gets `list`). To resize a slab/extrusion, remove and recreate the representation instead: ``` # 1. Unassign and remove old representation ifc_edit("geometry.unassign_representation", '{"product": "", "representation": ""}') ifc_edit("geometry.remove_representation", '{"representation": ""}') # 2. Create new representation with correct dimensions ifc_edit("geometry.add_slab_representation", '{"context": "11", "depth": 0.8, "polyline": "[[0,0],[0.6,0],[0.6,0.5],[0,0.5]]"}') # 3. Assign new representation ifc_edit("geometry.assign_representation", '{"product": "", "representation": ""}') ``` Note: the `polyline` parameter must be passed as a **JSON string** (not a bare array). ### Tracing geometry chains Walk entity references to find extrusion depths, clipping planes, etc.: ``` ifc_info() # → Representation → IfcProductDefinitionShape id ifc_info() # → Representations → IfcShapeRepresentation id (Body) ifc_info() # → Items → IfcExtrudedAreaSolid or IfcBooleanClippingResult ``` Batch parallel `ifc_info` calls to speed up traversal across many elements. --- ## Quantity Take-Off (QTO) Run `ifc_quantify` to compute quantities. Results are written as `IfcElementQuantity` property sets on each element. ``` ifc_quantify("IFC4QtoBaseQuantities") # IFC4 models ifc_quantify("IFC4X3QtoBaseQuantities") # IFC4x3 models ifc_quantify("IFC4QtoBaseQuantities", "IfcWall") # restrict to element type ifc_save() # persist to disk ``` **Weight availability by element type (IFC4QtoBaseQuantities):** | Element | Weight? | |---|---| | IfcWall, IfcSlab, IfcFooting, IfcBeam, IfcColumn | ✓ GrossWeight, NetWeight | | IfcWindow, IfcDoor | ✗ Area/dimensions only | | IfcRoof | ✗ GrossArea only | | IfcCovering | ✗ GrossArea/Width only | | IfcPipeSegment | ✗ Length only | Weight is computed only for structural/volumetric elements with known material density. --- ## Construction Scheduling ### Schedule structure ``` IfcWorkPlan └─ IfcWorkSchedule ├─ IfcTask "P1: Foundations" (summary) │ ├─ IfcTask "P1.1: Install Ground Beams" (leaf, has IfcTaskTime) │ └─ IfcTask "P1.2: Pour Floor Slab" (leaf, has IfcTaskTime) ├─ IfcTask "P2: Structure" (summary, has IfcTaskTime) │ └─ IfcTask "P2.1: Erect Walls" (leaf, has IfcTaskTime) └─ ... ``` Physical elements are linked to tasks via `IfcRelAssignsToProcess`. Task ordering is via `IfcRelSequence` (FINISH_START, START_START, etc.). ### Creating a schedule (MCP recipe) ``` # Work plan and schedule ifc_edit("sequence.add_work_plan", '{"name": "Construction Plan", "predefined_type": "ACTUAL"}') ifc_edit("sequence.add_work_schedule", '{"name": "Construction Schedule", "predefined_type": "PLANNED", "work_plan": ""}') # Phase (summary) tasks ifc_edit("sequence.add_task", '{"work_schedule": "", "name": "Foundations", "identification": "P1", "predefined_type": "CONSTRUCTION"}') # Leaf tasks ifc_edit("sequence.add_task", '{"parent_task": "", "name": "Install Ground Beams", "identification": "P1.1", "predefined_type": "CONSTRUCTION"}') # Assign physical elements to tasks ifc_edit("sequence.assign_process", '{"relating_process": "", "related_object": ""}') # Sequence relationships between phases/tasks ifc_edit("sequence.assign_sequence", '{"relating_process": "", "related_process": "", "sequence_type": "FINISH_START"}') # Add IfcTaskTime to ALL tasks (leaf AND summary — required for cascade to propagate) ifc_edit("sequence.add_task_time", '{"task": ""}') # → returns IfcTaskTime id # Set durations and start dates (use attribute.edit_attributes — see bug note below) ifc_edit("attribute.edit_attributes", '{"product": "", "attributes": "{\"ScheduleStart\": \"2026-03-02T09:00:00\", \"ScheduleDuration\": \"P5D\"}"}') # Cascade dates forward through sequence relationships ifc_edit("sequence.cascade_schedule", '{"task": ""}') # Verify ifc_schedule() ``` ### Work calendar (Mon–Fri) ``` ifc_edit("sequence.add_work_calendar", '{"name": "Mon-Fri Work Week"}') ifc_edit("sequence.add_work_time", '{"work_calendar": "", "time_type": "WorkingTimes"}') ifc_edit("sequence.assign_recurrence_pattern", '{"parent": "", "recurrence_type": "WEEKLY"}') ifc_edit("sequence.edit_recurrence_pattern", '{"recurrence_pattern": "", "attributes": "{\"WeekdayComponent\": [1,2,3,4,5]}"}') ``` `WeekdayComponent` uses integers 1–7 where 1=Monday, 7=Sunday. ### cascade_schedule limitations - Every task in the chain (summary AND leaf) must have an `IfcTaskTime` — tasks without one have null start/finish and block propagation to successors - Cascade propagates through `IfcRelSequence` relationships only, not into subtasks of a summary task. After cascade sets a summary task's start, its leaf subtasks must have their starts set manually (or connected via additional leaf-to-leaf sequences) - Summary task finish = start + duration; without a duration, finish equals start, propagating wrong dates to successors - `cascade_schedule` does **not** automatically respect a work calendar; weekend-skipping must be applied manually when setting task dates ### Known bug: `sequence.edit_task_time` silently fails for datetime fields `sequence.edit_task_time` returns `{"ok": true}` but does **not** persist `ScheduleStart` or `ScheduleFinish` changes. Use `attribute.edit_attributes` on the `IfcTaskTime` entity instead: ```python # WRONG — silently does nothing for datetime fields: ifc_edit("sequence.edit_task_time", '{"task_time": "123", "attributes": "{\"ScheduleStart\": \"2026-03-09T09:00:00\"}"}') # CORRECT: ifc_edit("attribute.edit_attributes", '{"product": "123", "attributes": "{\"ScheduleStart\": \"2026-03-09T09:00:00\"}"}') ``` `sequence.edit_task_time` works correctly for `ScheduleDuration` — only datetime fields are affected. --- ## Bill of Quantities (BoQ) ### Concepts IFC separates **quantities** (how much) from **pricing** (how much it costs). They are independent layers: - **Quantities** live on elements as `IfcElementQuantity` property sets (written by `ifc_quantify`). These must exist before building a cost schedule. - **Pricing** lives in a separate `IfcCostSchedule` → `IfcCostItem` tree. Unit rates are stored on `IfcCostValue` entities attached to cost items, not on elements. - At BoQ generation time, a tool multiplies `unit rate × quantity` to compute line-item totals. - The same element can appear in multiple cost schedules with different rates; rates can be updated without touching geometry. ### Cost schedule structure ``` IfcCostSchedule "Bill of Quantities" (predefined_type: COSTPLAN or PRICEDBILLOFQUANTITIES) └─ IfcCostItem "A - Substructure" (section/summary — has IfcCostValue with Category="*") ├─ IfcCostItem "A.1 - Ground Beams" (leaf — has IfcCostValue with unit rate) │ linked to IfcFooting elements via IfcRelAssignsToControl └─ IfcCostItem "A.2 - Floor Slab" linked to IfcSlab elements via IfcRelAssignsToControl ``` Elements are linked to cost items via `assign_cost_item_quantity`, which also creates the `IfcRelAssignsToControl` relationship automatically. ### Quantity names by element type | Element | Useful quantity names | |---|---| | IfcWall, IfcSlab, IfcFooting | `NetVolume`, `GrossVolume` | | IfcWindow, IfcDoor | `Area` | | IfcRoof, IfcCovering | `GrossArea`, `NetArea` | | IfcPipeSegment | `Length` | ### MCP recipe — creating a BoQ from scratch ``` # 1. Ensure quantities are computed first ifc_quantify("IFC4QtoBaseQuantities") # 2. Create the cost schedule ifc_edit("cost.add_cost_schedule", '{"name": "Bill of Quantities", "predefined_type": "COSTPLAN"}') # → returns IfcCostSchedule id (e.g. 100) # 3. Create section (summary) items at top level ifc_edit("cost.add_cost_item", '{"cost_schedule": "100"}') # → returns IfcCostItem id (e.g. 101) ifc_edit("cost.edit_cost_item", '{"cost_item": "101", "attributes": "{\"Name\": \"Substructure\", \"Identification\": \"A\"}"}') # 4. Create leaf items under a section ifc_edit("cost.add_cost_item", '{"cost_item": "101"}') # → returns id (e.g. 102) ifc_edit("cost.edit_cost_item", '{"cost_item": "102", "attributes": "{\"Name\": \"Ground Beams\", \"Identification\": \"A.1\"}"}') # 5. Link elements parametrically — quantities update automatically if geometry changes ifc_edit("cost.assign_cost_item_quantity", '{"cost_item": "102", "products": "10,11,12,13", "prop_name": "NetVolume"}') # 6. Add a unit rate to the leaf item ifc_edit("cost.add_cost_value", '{"parent": "102"}') # → returns IfcCostValue id (e.g. 200) ifc_edit("cost.edit_cost_value", '{"cost_value": "200", "attributes": "{\"AppliedValue\": 350.0}"}') # 7. Add a sum value to section items (Category="*" marks it as a subtotal) # This is required for PDF export — compute total = sum(quantity × rate) for each section ifc_edit("cost.add_cost_value", '{"parent": "101"}') # → returns id (e.g. 201) ifc_edit("cost.edit_cost_value", '{"cost_value": "201", "attributes": "{\"Category\": \"*\", \"AppliedValue\": 2500.0}"}') # 8. Verify and save ifc_cost() ifc_validate() ifc_save() ``` ### Key API notes - `cost.add_cost_item` accepts no `name` parameter — set name afterwards with `cost.edit_cost_item` - `cost.edit_cost_item` and `cost.edit_cost_value` require `attributes` as a **JSON string** (not a dict): ``` '{"attributes": "{\"Name\": \"Ground Beams\"}"}' # correct — double-encoded '{"attributes": {"Name": "Ground Beams"}}' # WRONG — will error ``` - `assign_cost_item_quantity` with `prop_name=""` counts elements instead of summing a named quantity - `AppliedValue` accepts a plain float (stored as `IfcMonetaryMeasure`) ### Section items MUST have Category="*" cost values Section/summary items with no quantities and no `Category="*"` cost value have `ItemIsASum=False` in the export. This causes the PDF exporter to try parsing their empty `Quantity` field as a float and crash. Every section item must have an `IfcCostValue` with `Category="*"` and `AppliedValue` set to the pre-computed section total. ### Exporting to PDF The `ifc5d` PDF writer produces the best-looking output. The format flag requires **uppercase**: ```bash # PDF — invoke directly (not wired into the CLI format switch) python3 -c " import ifcopenshell from ifc5d.ifc5Dspreadsheet import Ifc5DPdfWriter f = ifcopenshell.open('model.ifc') cs = next(iter(f.by_type('IfcCostSchedule'))) Ifc5DPdfWriter(file=f, output='boq.pdf', options={}, cost_schedule=cs, force_schedule_type='PRICEDBILLOFQUANTITIES').write() " # XLSX (via CLI — uppercase format required) python3 -m ifc5d.ifc5Dspreadsheet -f XLSX model.ifc ./output_dir/ # CSV python3 -m ifc5d.ifc5Dspreadsheet -f CSV model.ifc ./output_dir/ ``` **Requires `typst` for PDF:** `pip install typst` **Known ifc5d limitations:** - The `-f` flag is case-sensitive: `XLSX`, `CSV`, `ODS` (lowercase silently fails with `NameError: writer`) - PDF is not in the CLI format switch — must be called directly from Python (see above) - XLSX/CSV `TotalPrice` column is always 0 for unit-rate items; totals are only computed as spreadsheet formulas in the ODS writer (which has a column-name bug), and at render time in the typst PDF template - The typst PDF template computes `Total = Quantity × RateSubtotal` for leaf items; for section items it reads `TotalPrice` from the `Category="*"` cost value --- ## Validation and CI/CD ### Manual validation ``` ifc_validate() # schema validation (fast) ifc_validate(express_rules=True) # full EXPRESS rules (slower, more thorough) ``` Or via CLI: ```bash python3 -m ifcquery validate python3 -m ifcquery validate --rules ``` ### IDS validation IDS (Information Delivery Specification) files define requirements that models must satisfy — required property sets, classification codes, material assignments, etc. Validate with `ifctester`: ```bash python3 -m ifctester ``` IDS checks are typically run in CI on every commit or pull request. **Reading ifctester output:** - `[PASS] (0/0)` — no elements matched the applicability filter; check trivially passes (`minOccurs="0"`) - `[PASS] (5/5)` — 5 elements matched and all 5 passed the requirement - `[FAIL] (0/5)` — 5 elements matched but none passed; the failure lines list each failing element **IDS structure:** each specification has two parts: - `` — filter selecting which elements the check applies to (AND logic across multiple facets) - `` — what those elements must have (property name, value, pset name) **Fixing IDS failures — workflow:** 1. Read the IDS file to understand exactly what property name, pset name, and value are required 2. Use `ifc_info()` to inspect the failing element's current property sets 3. Note whether multiple elements share the same `IfcPropertySet` entity (same `id`) — editing it once fixes all of them 4. Use `pset.edit_pset` to add missing properties: ``` ifc_edit("pset.edit_pset", '{"pset": "", "properties": "{\"FireRating\": \"30\"}", "should_purge": "false"}') ``` Use `should_purge: false` to add/update properties without removing existing ones. **Critical: IDS property names are exact string matches.** A property named `Thermal Transmitance` (space, single-t) is entirely different from the IFC standard `ThermalTransmittance`. Always copy property names character-for-character from the IDS `` element — do not assume they match standard IFC pset property names. **Shared psets:** in IFC, multiple elements (instances and their type) often share the same `IfcPropertySet` entity via `IfcRelDefinesByProperties`. Check the `"id"` field in `ifc_info` property set output — if 4 walls and a WallType all show the same pset id, one `pset.edit_pset` call fixes all of them simultaneously. **Known ifctester quirk with boolean applicability filters:** ifctester may flag elements even when their boolean property value does not match the applicability filter (e.g., an `IsExternal=True` wall being matched by an `IsExternal=False` filter). Treat the reported failing elements as authoritative — add whatever the requirement asks for, regardless of whether the applicability logic seems correct. **`?` is NOT a wildcard in ifctester — it matches the literal string `"?"`.** To check that an attribute has any non-empty value, use an `xs:restriction` pattern instead: ```xml Name ? Name ``` This applies to `` requirements. The `xmlns:xs="http://www.w3.org/2001/XMLSchema"` namespace must be declared on the root `` element (it is in the standard IDS template). ### ifcopenshell.validate Lower-level programmatic validation: ```bash python3 -c "import ifcopenshell; import ifcopenshell.validate; f = ifcopenshell.open(''); ifcopenshell.validate.validate(f)" ``` --- ## Bonsai Asset Libraries Bonsai ships IFC4 asset libraries at: ``` /usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/ ``` Key libraries: - `IFC4 Furniture Library.ifc` — 150 `IfcFurnitureType` entries (beds, chairs, sofas, tables, desks, kitchen units, etc.) - `IFC4 Landscape Library.ifc` — 66 `IfcGeographicElementType` entries (Apple, Round Tree, Conical Tree Small (5m), Shrub, etc.) - `IFC4 Entourage Library.ifc` — `IfcBuildingElementProxyType` people figures ### Appending a library type Use `ifc_edit("project.append_asset", ...)` via MCP if available. At the time of writing, the MCP coercion layer had no handler for `ifcopenshell.file`-typed parameters, so the `library` argument could not be passed through `ifc_edit` — this may have since been fixed in ifcedit/ifcmcp. If MCP does not work, fall back to Python: ```python import ifcopenshell, ifcopenshell.api.project, ifcopenshell.api.type LIB = '/usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/IFC4 Landscape Library.ifc' f = ifcopenshell.open('model.ifc') lib = ifcopenshell.open(LIB) # Find the type in the library apple_type_in_lib = next(t for t in lib.by_type('IfcGeographicElementType') if t.Name == 'Apple') # Copy into the model — returns the new type entity in f apple = ifcopenshell.api.project.append_asset(f, library=lib, element=apple_type_in_lib) # Assign to an occurrence tree = f.by_id() ifcopenshell.api.type.assign_type(f, related_objects=[tree], relating_type=apple) f.write('model.ifc') ``` `append_asset` copies all dependent geometry, materials, and styles. The occurrence keeps its own name, placement, and properties while getting the type's geometry. --- ## Git Workflow IFC files use STEP Physical File format — plain text, one entity per line — which is natively git-friendly. Commits, diffs, rollbacks, and blame all work as expected. **However, IFC branches do not merge correctly with standard git** because STEP IDs are file-scoped integers that conflict across branches. Use `ifcmerge` as the merge driver: ### Configuring ifcmerge `.gitattributes` is already committed to this repo with `*.ifc merge=ifcmerge`. Add the driver to `.git/config` or `~/.gitconfig`: ``` [merge "ifcmerge"] name = IFC merge driver driver = /home/bruno/src/ifcmerge/ifcmerge %O %A %B %P ``` ### ifcmerge behaviour - By default ifcmerge **prefers the `$REMOTE` branch** and rewrites step IDs in `$LOCAL` to avoid conflicts. This is the correct behaviour when merging from `main`/`origin` into your local branch (i.e. a `git pull` or `git merge main`). - When merging a **feature branch into main** (i.e. you want `$LOCAL`=main to be the authoritative base), reverse the driver arguments so that main's step IDs are preserved: ``` driver = ifcmerge %O %B %A %P ``` The easiest approach is to merge in the other direction (merge main into the feature branch first to resolve conflicts there, then fast-forward main), which keeps the default driver order correct. ### Typical branch workflow - `main` — validated, CI-passing models only - Feature branches for each change (new elements, schedule additions, geometry edits, etc.) - Open a pull request → CI runs `ifc_validate` and IDS checks → merge via ifcmerge on approval - Tag releases for milestone model snapshots --- ## Tips - **Always validate before saving** — run `ifc_validate()` after edits and before `ifc_save()` - **`ifc_info` on non-product entities** — works on any step ID including geometry, task times, placements; useful for tracing chains - **Batch parallel tool calls** — independent `ifc_info` and `ifc_edit` calls can be issued in parallel for speed - **`ifc_edit` does not auto-save** — always call `ifc_save()` explicitly when done with a set of edits - **ISO 8601 durations** — task durations use `P5D` (5 days), `P1W` (1 week), `PT8H` (8 hours) - **Step IDs are file-specific** — never hard-code step IDs; always query first with `ifc_select`, `ifc_tree`, or `ifc_info` - **Space boundaries** — when adding, moving, or deleting windows and doors, check for `IfcRelSpaceBoundary` relationships that may also need updating