mirror of
https://github.com/brunopostle/simple-ifc.git
synced 2026-03-30 06:53:18 +02:00
796 lines
38 KiB
Markdown
796 lines
38 KiB
Markdown
# 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 <file> <subcommand> [args]
|
||
python3 -m ifcedit <subcommand> [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(<id>)
|
||
ifc_relations(<id>)
|
||
|
||
# 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(<window_id>)` → note `filled_void` → IfcOpeningElement ID
|
||
2. **Find space boundaries**: `ifc_relations(<window_id>)` → note any `IfcRelSpaceBoundary` IDs referencing this element
|
||
3. **Delete the window**: `ifc_edit("root.remove_product", '{"product": "<window_id>"}')`
|
||
4. **Delete the orphaned opening**: `ifc_edit("root.remove_product", '{"product": "<opening_id>"}')`
|
||
|
||
`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(<wall_id>) # → check children.openings for unfilled IfcOpeningElements
|
||
```
|
||
|
||
### Moving a window or door
|
||
|
||
```
|
||
ifc_edit("geometry.edit_object_placement", '{"product": "<window_id>", "matrix": [[...],[...],[...],[0,0,0,1]]}')
|
||
ifc_edit("geometry.edit_object_placement", '{"product": "<opening_id>", "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": "<window_id>", "relating_type": "<type_id>"}')
|
||
```
|
||
|
||
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": "<id>", "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": "<element_id>", "representation": "<rep_id>"}')
|
||
ifc_edit("geometry.remove_representation", '{"representation": "<rep_id>"}')
|
||
|
||
# 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": "<element_id>", "representation": "<new_rep_id>"}')
|
||
```
|
||
|
||
Note: the `polyline` parameter must be passed as a **JSON string** (not a bare array).
|
||
|
||
### Wall mitre connections
|
||
|
||
`geometry.connect_wall` + `geometry.regenerate_wall_representation` produce properly mitered wall ends where walls meet at corners. The workflow:
|
||
|
||
```
|
||
# 1. Add an Axis/Curve2D representation to each wall (context 23 = Plan/GRAPH_VIEW)
|
||
# axis is a list of 2D points [[x0,y0],[x1,y1]] in the wall's LOCAL frame
|
||
ifc_edit("geometry.add_axis_representation", '{"context": "23", "axis": "[[0,0],[4.0,0]]"}')
|
||
ifc_edit("geometry.assign_representation", '{"product": "<wall_id>", "representation": "<axis_rep_id>"}')
|
||
|
||
# 2. Connect the walls (creates IfcRelConnectsPathElements)
|
||
ifc_edit("geometry.connect_wall", '{"wall1": "<wall_a>", "wall2": "<wall_b>"}')
|
||
|
||
# 3. Regenerate to compute miter cuts
|
||
ifc_edit("geometry.regenerate_wall_representation", '{"wall": "<wall_id>", "length": "4.0", "height": "2.5"}')
|
||
```
|
||
|
||
`connect_wall` must be called for both orientations (A→B and B→A) for a two-wall corner. All connected walls must be regenerated.
|
||
|
||
**CRITICAL: `regenerate_wall_representation` removes slope clippings.** Do not use it on walls with top-clipped (sloped) geometry.
|
||
|
||
**For sloped walls needing a mitre**, use `ifc_shape` to bake the 45° cut into the 2D profile as a trapezoid, then apply the slope as a single `IfcHalfSpaceSolid` via `geometry.add_boolean`:
|
||
|
||
```
|
||
# South porch wall: mitre cuts from (1.9,0) to (1.57,0.33) at east end
|
||
# → trapezoid profile: (0,0)→(1.9,0)→(1.57,0.33)→(0,0.33)
|
||
ifc_shape("polyline", {"points": [[0,0],[1.9,0],[1.57,0.33],[0,0.33]], "closed": true})
|
||
# → polyline_id
|
||
ifc_shape("profile", {"outer_curve": <polyline_id>}) # → profile_id
|
||
ifc_shape("extrude", {"profile_or_curve": <profile_id>, "magnitude": 3.5, "position": [0,0,0]})
|
||
# → extrusion_id
|
||
ifc_shape("get_representation", {"context": 11, "items": [<extrusion_id>]}) # → rep_id (SweptSolid)
|
||
|
||
# Create the slope half-space (normal points INTO the kept region below the slope)
|
||
ifc_shape("plane", {"location": [0,0,3.26], "normal": [-0.419, 0, -0.908]}) # → plane_id
|
||
ifc_shape("half_space_solid", {"plane": <plane_id>, "agreement_flag": false}) # → hss_id
|
||
|
||
# Apply slope as boolean — moves extrusion out of rep top-level, adds BCR in its place
|
||
ifc_edit("geometry.add_boolean", '{"first_item": "<extrusion_id>", "second_items": "<hss_id>"}')
|
||
ifc_edit("attribute.edit_attributes", '{"product": "<rep_id>", "attributes": "{\"RepresentationType\": \"Clipping\"}"}')
|
||
|
||
# Swap body rep
|
||
ifc_edit("geometry.unassign_representation", '{"product": "<wall_id>", "representation": "<old_rep_id>"}')
|
||
ifc_edit("geometry.remove_representation", '{"representation": "<old_rep_id>"}')
|
||
ifc_edit("geometry.assign_representation", '{"product": "<wall_id>", "representation": "<rep_id>"}')
|
||
```
|
||
|
||
**Mitre trapezoid geometry** — for a wall with thickness `t=0.33`, mitre at the east end (local x=L):
|
||
- `t_at_mitre = t` (both walls same thickness → 45°)
|
||
- East-end mitre: profile points `(0,0)→(L,0)→(L−t, t)→(0,t)` (removes NE inner corner)
|
||
- West-end mitre: profile points `(0,0)→(L,0)→(L,t)→(t,t)` (removes NW inner corner, closing from `(t,t)` to `(0,0)`)
|
||
- For walls with origin at the mitre end: profile points `(0,0)→(L,0)→(L,t)→(t,t)` where x=0 is the mitre end
|
||
|
||
**Slope normal** must point INTO the kept region (below slope = negative Z component):
|
||
- Slope rising west→east: normal `[+0.419, 0, −0.908]` at location `[0, 0, z_at_x0]`
|
||
- Slope falling west→east: normal `[−0.419, 0, −0.908]` at location `[0, 0, z_at_x0]`
|
||
|
||
**Do NOT use dual `add_wall_representation` clippings** (two half-spaces: slope + mitre) — this produces incorrect geometry in viewers even though the IFC math is sound.
|
||
|
||
### Tracing geometry chains
|
||
|
||
Walk entity references to find extrusion depths, clipping planes, etc.:
|
||
|
||
```
|
||
ifc_info(<wall_id>) # → Representation → IfcProductDefinitionShape id
|
||
ifc_info(<pds_id>) # → Representations → IfcShapeRepresentation id (Body)
|
||
ifc_info(<body_rep_id>) # → Items → IfcExtrudedAreaSolid or IfcBooleanClippingResult
|
||
```
|
||
|
||
Batch parallel `ifc_info` calls to speed up traversal across many elements.
|
||
|
||
---
|
||
|
||
## Custom Geometry with the Shape Builder
|
||
|
||
`ifc_shape_list()` and `ifc_shape_docs(method)` expose `ShapeBuilder` — ifcopenshell's geometry construction API. `ifc_shape(method, params)` calls a ShapeBuilder method and returns the created entity's step ID.
|
||
|
||
### Key methods
|
||
|
||
| Method | Returns | Notes |
|
||
|---|---|---|
|
||
| `circle(center, radius)` | `IfcCircle` | 2D profile for round extrusions |
|
||
| `rectangle(size, position)` | `IfcIndexedPolyCurve` | 2D rectangular polyline |
|
||
| `profile(outer_curve, ...)` | `IfcArbitraryClosedProfileDef` | Wraps a curve as an area profile |
|
||
| `extrude(profile_or_curve, magnitude, position, ...)` | `IfcExtrudedAreaSolid` | Curves auto-converted to profiles |
|
||
| `block(position, x_length, y_length, z_length)` | `IfcBlock` | CSG primitive — **avoid mixing with SweptSolid items** |
|
||
| `polyline(points, closed, ...)` | `IfcIndexedPolyCurve` | General 2D curve |
|
||
| `get_representation(context, items)` | `IfcShapeRepresentation` | Bundles items into a representation |
|
||
|
||
### Parameter coercion in `ifc_shape`
|
||
|
||
- `entity_instance` params: pass as integer step ID (`7832`), string (`"7832"`), or `"#7832"`
|
||
- `Sequence[entity_instance]` params (e.g. `items` in `get_representation`): pass as a **JSON list** of integer step IDs: `[7839, 7846, 7853]`
|
||
- Vectors: JSON arrays `[1.0, 0.0, 0.0]`
|
||
|
||
**Bug fixed in `ifcmcp/core.py`:** early versions didn't coerce `Sequence[entity_instance]` — `_coerce_shape_value` now handles `collections.abc.Sequence` origins, resolving each element to an entity.
|
||
|
||
### Critical: `IfcShapeRepresentation` items must be homogeneous
|
||
|
||
All items in one `IfcShapeRepresentation` must share the same representation type — mixing is a validation error:
|
||
|
||
| Item type | RepresentationType |
|
||
|---|---|
|
||
| `IfcExtrudedAreaSolid` | `SweptSolid` |
|
||
| `IfcBlock`, `IfcSphere` | `CSG` |
|
||
| `IfcFacetedBrep` | `Brep` |
|
||
|
||
**Prefer `rectangle` → `extrude` over `block`** so all items stay `SweptSolid`:
|
||
|
||
```
|
||
# CORRECT — rectangle profile extruded (SweptSolid, mixes safely with extruded legs)
|
||
ifc_shape("rectangle", {"size": [1.4, 0.8], "position": [-0.7, -0.4]}) # → IfcIndexedPolyCurve
|
||
ifc_shape("extrude", {"profile_or_curve": <id>, "magnitude": 0.04, "position": [0, 0, 0.72]})
|
||
|
||
# WRONG — IfcBlock is CSG; mixing with SweptSolid legs fails validation
|
||
ifc_shape("block", {"position": [-0.7, -0.4, 0.72], "x_length": 1.4, ...})
|
||
```
|
||
|
||
### Full recipe — custom shaped furniture with a type
|
||
|
||
```
|
||
# 1. Build geometry (all SweptSolid)
|
||
ifc_shape("circle", {"center": [0, 0], "radius": 0.03}) # → circle_id
|
||
ifc_shape("extrude", {"profile_or_curve": <circle_id>, "magnitude": 0.72, "position": [-0.6, -0.3, 0]}) # leg 1
|
||
# ... 3 more legs at (+0.6,-0.3), (-0.6,+0.3), (+0.6,+0.3) ...
|
||
ifc_shape("rectangle", {"size": [1.4, 0.8], "position": [-0.7, -0.4]}) # → rect_id
|
||
ifc_shape("extrude", {"profile_or_curve": <rect_id>, "magnitude": 0.04, "position": [0, 0, 0.72]}) # tabletop
|
||
|
||
# 2. Bundle into one representation
|
||
ifc_shape("get_representation", {"context": 11, "items": [<leg1>, <leg2>, <leg3>, <leg4>, <tabletop>]})
|
||
# → IfcShapeRepresentation (RepresentationType auto-set to "SweptSolid")
|
||
|
||
# 3. Create type, assign representation to it
|
||
ifc_edit("root.create_entity", '{"ifc_class": "IfcFurnitureType", "name": "dining table type", "predefined_type": "TABLE"}')
|
||
ifc_edit("geometry.assign_representation", '{"product": "<type_id>", "representation": "<rep_id>"}')
|
||
|
||
# 4. Create occurrence, set placement, container, then assign type (auto-maps geometry)
|
||
ifc_edit("root.create_entity", '{"ifc_class": "IfcFurniture", "name": "dining table"}')
|
||
ifc_edit("geometry.edit_object_placement", '{"product": "<furn_id>", "matrix": "[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]"}')
|
||
ifc_edit("spatial.assign_container", '{"products": "<furn_id>", "relating_structure": "<space_id>"}')
|
||
ifc_edit("type.assign_type", '{"related_objects": "<furn_id>", "relating_type": "<type_id>"}')
|
||
# → creates IfcMappedItem on the occurrence pointing to the type's RepresentationMap
|
||
|
||
ifc_validate()
|
||
ifc_save()
|
||
```
|
||
|
||
### Swapping a type's representation
|
||
|
||
When replacing the geometry on an already-typed element:
|
||
|
||
```
|
||
# 1. Build new geometry and assign to type
|
||
ifc_shape(...)
|
||
ifc_shape("get_representation", ...) # → new_rep_id
|
||
ifc_edit("geometry.assign_representation", '{"product": "<type_id>", "representation": "<new_rep_id>"}')
|
||
|
||
# 2. Unassign old instance mapping and old type rep, then remove old rep
|
||
ifc_edit("geometry.unassign_representation", '{"product": "<instance_id>", "representation": "<old_mapped_rep_id>"}')
|
||
ifc_edit("geometry.unassign_representation", '{"product": "<type_id>", "representation": "<old_rep_id>"}')
|
||
ifc_edit("geometry.remove_representation", '{"representation": "<old_rep_id>"}')
|
||
|
||
# 3. Re-run assign_type to create a fresh MappedItem from the new type rep
|
||
ifc_edit("type.assign_type", '{"related_objects": "<instance_id>", "relating_type": "<type_id>"}')
|
||
|
||
# 4. Clean up orphans — calling assign_type a second time leaves the first MappedRepresentation
|
||
# orphaned (OfProductRepresentation=() AND RepresentationMap=() violates EXPRESS XOR constraint)
|
||
ifc_validate(express_rules=True) # reports orphaned rep by id
|
||
ifc_edit("geometry.remove_representation", '{"representation": "<orphan_id>"}')
|
||
# Note: removing one orphan may transitively delete related ones
|
||
```
|
||
|
||
---
|
||
|
||
## 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": "<plan_id>"}')
|
||
|
||
# Phase (summary) tasks
|
||
ifc_edit("sequence.add_task", '{"work_schedule": "<sched_id>", "name": "Foundations", "identification": "P1", "predefined_type": "CONSTRUCTION"}')
|
||
|
||
# Leaf tasks
|
||
ifc_edit("sequence.add_task", '{"parent_task": "<phase_id>", "name": "Install Ground Beams", "identification": "P1.1", "predefined_type": "CONSTRUCTION"}')
|
||
|
||
# Assign physical elements to tasks
|
||
ifc_edit("sequence.assign_process", '{"relating_process": "<task_id>", "related_object": "<element_id>"}')
|
||
|
||
# Sequence relationships between phases/tasks
|
||
ifc_edit("sequence.assign_sequence", '{"relating_process": "<pred_id>", "related_process": "<succ_id>", "sequence_type": "FINISH_START"}')
|
||
|
||
# Add IfcTaskTime to ALL tasks (leaf AND summary — required for cascade to propagate)
|
||
ifc_edit("sequence.add_task_time", '{"task": "<task_id>"}') # → returns IfcTaskTime id
|
||
|
||
# Set durations and start dates (use attribute.edit_attributes — see bug note below)
|
||
ifc_edit("attribute.edit_attributes", '{"product": "<task_time_id>", "attributes": "{\"ScheduleStart\": \"2026-03-02T09:00:00\", \"ScheduleDuration\": \"P5D\"}"}')
|
||
|
||
# Cascade dates forward through sequence relationships
|
||
ifc_edit("sequence.cascade_schedule", '{"task": "<first_task_id>"}')
|
||
|
||
# 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": "<cal_id>", "time_type": "WorkingTimes"}')
|
||
ifc_edit("sequence.assign_recurrence_pattern", '{"parent": "<work_time_id>", "recurrence_type": "WEEKLY"}')
|
||
ifc_edit("sequence.edit_recurrence_pattern", '{"recurrence_pattern": "<pat_id>", "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 <file> validate
|
||
python3 -m ifcquery <file> 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_file> <ifc_file>
|
||
```
|
||
|
||
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:
|
||
- `<applicability>` — filter selecting which elements the check applies to (AND logic across multiple facets)
|
||
- `<requirements>` — 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(<element_id>)` 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": "<pset_id>", "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 `<baseName>` 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.
|
||
|
||
**`<simpleValue>?</simpleValue>` 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
|
||
<!-- WRONG — matches only the literal string "?" -->
|
||
<attribute>
|
||
<name><simpleValue>Name</simpleValue></name>
|
||
<value><simpleValue>?</simpleValue></value>
|
||
</attribute>
|
||
|
||
<!-- CORRECT — matches any non-empty string -->
|
||
<attribute>
|
||
<name><simpleValue>Name</simpleValue></name>
|
||
<value>
|
||
<xs:restriction base="xs:string">
|
||
<xs:pattern value=".+"/>
|
||
</xs:restriction>
|
||
</value>
|
||
</attribute>
|
||
```
|
||
|
||
This applies to `<attribute>` requirements. The `xmlns:xs="http://www.w3.org/2001/XMLSchema"` namespace must be declared on the root `<ids>` 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('<file>'); 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
|
||
|
||
The `library` parameter is typed `file_path` — ifcmcp opens the file automatically from the path string. Use `ifc_edit` directly:
|
||
|
||
```
|
||
# 1. Find the element ID in the library file
|
||
# (load it temporarily, query, then reload your model)
|
||
ifc_load("/usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/IFC4 Landscape Library.ifc")
|
||
ifc_select("IfcGeographicElementType") # find the "Apple" entry and note its id
|
||
|
||
# 2. Reload your model, then append the asset by library path + element id
|
||
ifc_load("model.ifc")
|
||
ifc_edit("project.append_asset", '{"library": "/usr/lib/python3.14/site-packages/bonsai/bim/data/libraries/IFC4 Landscape Library.ifc", "element": "<type_id_from_library>"}')
|
||
# → returns the new type entity id in the active model
|
||
|
||
# 3. Assign the type to an occurrence
|
||
ifc_edit("type.assign_type", '{"related_objects": "<occurrence_id>", "relating_type": "<new_type_id>"}')
|
||
|
||
ifc_save()
|
||
```
|
||
|
||
`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`.
|
||
|
||
**Warning:** if `.gitattributes` is not in place (or the merge driver is not configured) when branches are merged, git's standard text merge will silently drop new entities from one branch when two branches have both added entities starting at the same step ID. Always confirm the merge driver is configured before merging IFC branches.
|
||
|
||
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
|
||
- **Representation type homogeneity** — all items in one `IfcShapeRepresentation` must be the same type (SweptSolid, CSG, Brep, etc.); mixing causes a validation error. Use `rectangle` → `extrude` rather than `block` to keep everything `SweptSolid`
|
||
- **Shape builder orphans** — after calling `type.assign_type` a second time, always run `ifc_validate(express_rules=True)` to catch orphaned `IfcShapeRepresentation` entities and remove them with `geometry.remove_representation`
|