Property Editing¶
IFClite supports editing IFC properties in-place with full change tracking, undo/redo, and export. The @ifc-lite/mutations package provides the mutation infrastructure, while the viewer integrates it with a property editor UI.
How It Works¶
Mutations are tracked through a MutablePropertyView that wraps the original read-only property table. When you edit a property:
- The original value is preserved
- The new value is stored in an overlay
- Reads return the mutated value transparently
- All changes are tracked as a
Mutationwith old/new values - Changes can be exported, applied to other models, and shared via change sets
Quick Start¶
Editing Properties¶
import { MutablePropertyView } from '@ifc-lite/mutations';
// Create a mutable view over the property table
// Parameters: (baseTable: PropertyTable | null, modelId: string)
const view = new MutablePropertyView(propertyTable, 'my-model');
// Set a property value
const mutation = view.setProperty(
entityId, // Express ID of the entity
'Pset_WallCommon', // Property set name
'FireRating', // Property name
'REI 120', // New value
);
console.log(`Changed from "${mutation.oldValue}" to "${mutation.newValue}"`);
// Read the mutated value
const value = view.getPropertyValue(entityId, 'Pset_WallCommon', 'FireRating');
// Returns 'REI 120'
Mutation History¶
// Get all mutations applied to this view
const mutations = view.getMutations();
// Check if an entity has changes
const hasChanges = view.hasChanges(entityId);
// Get count of modified entities
const count = view.getModifiedEntityCount();
// Clear all mutations (reset to original state)
view.clear();
Note: Undo/redo is handled by the viewer's store (mutationSlice), not directly on MutablePropertyView. In the viewer, use Ctrl+Z / Ctrl+Shift+Z.
Change Sets¶
Change sets group related mutations for export and sharing:
import { ChangeSetManager } from '@ifc-lite/mutations';
const manager = new ChangeSetManager();
// Create a change set (becomes the active change set)
const changeSet = manager.createChangeSet('Fire Safety Updates');
// Add mutations to the active change set
manager.addMutation(mutation1);
manager.addMutation(mutation2);
// Export as JSON
const json = manager.exportChangeSet(changeSet.id);
// Import on another instance
const imported = manager.importChangeSet(json);
Bulk Operations¶
For updating many entities at once, use the BulkQueryEngine:
import { BulkQueryEngine } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';
// Constructor requires EntityTable and MutablePropertyView
const engine = new BulkQueryEngine(entityTable, mutationView);
// Define a bulk query - which entities to update and how
const query = {
select: {
entityTypes: [10], // Type enum values (e.g., IfcWall)
propertyFilters: [{
psetName: 'Pset_WallCommon',
propName: 'IsExternal',
operator: '=' as const,
value: true,
}],
},
action: {
type: 'SET_PROPERTY' as const,
psetName: 'Pset_WallCommon',
propName: 'ThermalTransmittance',
value: 0.18,
valueType: PropertyValueType.Real,
},
};
// Preview changes before applying
const preview = engine.preview(query);
console.log(`Will update ${preview.matchedCount} entities`);
// Apply
const result = engine.execute(query);
console.log(`Updated ${result.affectedEntityCount} properties`);
CSV Import¶
Import property updates from spreadsheets:
import { CsvConnector } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';
// Constructor requires EntityTable and MutablePropertyView
const connector = new CsvConnector(entityTable, mutationView);
// Parse CSV (returns CsvRow[])
const rows = connector.parse(csvString, {
delimiter: ',',
hasHeader: true,
});
// Define mapping from CSV columns to IFC properties
const mapping = {
matchStrategy: { type: 'globalId' as const, column: 'GlobalId' },
propertyMappings: [
{ sourceColumn: 'Fire Rating', targetPset: 'Pset_WallCommon', targetProperty: 'FireRating', valueType: PropertyValueType.String },
{ sourceColumn: 'U-Value', targetPset: 'Pset_WallCommon', targetProperty: 'ThermalTransmittance', valueType: PropertyValueType.Real },
],
};
// Import (takes CSV string directly, not pre-parsed rows)
const stats = connector.import(csvString, mapping);
console.log(`Matched: ${stats.matchedRows}, Updated: ${stats.mutationsCreated}, Skipped: ${stats.unmatchedRows}`);
Viewer Integration¶
In the IFClite viewer:
- Select an entity in 3D or the hierarchy panel
- Open Properties panel — Edit properties directly in the panel
- Bulk edit — Use the Property Editor to update multiple entities
- Track changes — Modified properties are highlighted
- Undo/Redo — Ctrl+Z / Ctrl+Shift+Z to undo/redo edits
- Export — Save modified IFC with changes applied
Properties panel tabs¶
| Tab | Edits | Backed by |
|---|---|---|
| Properties | IfcRoot named attributes (Name, Description, …), property sets, classifications, materials, documents | setProperty / setAttribute |
| Quantities | Quantity sets and individual quantities | setQuantity |
| bSDD | Add buildingSMART Data Dictionary properties | setProperty |
| Raw STEP | Positional STEP arguments on the selected entity (one row per arg, inline pen-icon editor). Mutated rows show a purple dot. | setPositionalAttribute |
The Raw STEP tab is the right place for non-IfcRoot edits — IfcRectangleProfileDef.XDim, IfcCartesianPoint.Coordinates, anything without a symbolic attribute name.
Selection context menu¶
Right-click on an entity in 3D or the hierarchy:
| Item | Effect |
|---|---|
| Delete entity (red) | Tombstones the entity. Visible only when the active model has an editable mutation view. Toast confirms with undo hint. |
| Add Column here… (emerald) | Visible only when the right-clicked entity is an IfcBuildingStorey. Opens the Add Column dialog with the storey pre-filled. |
Add Column dialog¶
A modal triggered from the context menu or the "Column" button on the Edit Toolbar (when a storey is selected):
- Storey picker — sorted by elevation (bottom to top, matching the building) with each storey's elevation shown in metres.
- Position — storey-local X / Y / Z in metres.
- Cross-section — Width / Depth / Height with
> 0validation per field. - Name — defaults to
Column. - Optional metadata — Description / ObjectType / Tag, collapsed by default.
On submit, the dialog calls bim.store.addColumn, selects the newly-added column in the 3D scene, and shows a success toast. Anchor-resolution failures (e.g. a model without an IfcOwnerHistory) surface as an inline red alert inside the dialog rather than throwing.
Mutation State¶
| State | Description |
|---|---|
| Modified entities | Count of entities with property changes |
| Dirty models | Models with unsaved mutations |
| Undo stack | Per-model undo history (covers properties, quantities, attributes, positional args, entity create/delete) |
| Redo stack | Per-model redo history |
| Change sets | Named groups of mutations for export |
| Store editors | Per-model StoreEditor cache (created lazily on first store-level edit) |
Store-Level Editing¶
The mutation overlay also supports STEP-level edits — adding new entities, deleting existing ones, and overriding positional STEP arguments on entities that don't have named attributes (e.g. IfcRectangleProfileDef.XDim). This is the API surface behind the viewer's Raw STEP tab and the bim.store.* SDK / sandbox namespace.
Use the property/quantity APIs above for IfcRoot edits (Name, FireRating, …). Reach for StoreEditor when you need to edit a profile dimension, drop a new column into an existing model, or remove a stale entity.
StoreEditor — high-level API¶
import { MutablePropertyView, StoreEditor } from '@ifc-lite/mutations';
const view = new MutablePropertyView(propertyTable, modelId);
const editor = new StoreEditor(dataStore, view);
// Add a fresh entity with positional STEP attributes.
// Pass the canonical IFC EXPRESS PascalCase name; the public API surface
// (StoreEditor / bim.store) is consistently PascalCase. Internally,
// StepExporter upper-cases at the STEP write boundary.
const profile = editor.addEntity('IfcRectangleProfileDef', [
'.AREA.', null, '#34', 0.6, 0.4,
]);
// → { expressId: <new>, type: 'IfcRectangleProfileDef', byteOffset: -1, ... }
// Override a single positional argument on an existing entity by index.
// (STEP argument index is zero-based — index 0 = first STEP argument.)
editor.setPositionalAttribute(profile.expressId, 3, 0.7); // XDim → 0.7
// Remove an entity (existing entities are tombstoned, overlay-only ones forgotten).
editor.removeEntity(unwantedExpressId);
Edits accumulate in the same overlay used by setProperty / setAttribute. They land in the exported file the next time you call StepExporter.export({ applyMutations: true }).
STEP value conventions¶
addEntity and setPositionalAttribute accept the same value shape that EntityExtractor.extractEntity().attributes produces — keeping the read/write round-trip predictable:
| JS value | STEP literal |
|---|---|
null / undefined |
$ |
42 / 0.6 |
integer / REAL |
true / false |
.T. / .F. |
"#42" (string) |
entity reference |
".AREA." (string) |
enum |
"My Column" (string) |
quoted STEP string |
[1, 2, 3] |
STEP list (1,2,3) — recursive |
High-Level Builders — addColumnToStore / addWallToStore / …¶
For full element-with-geometry inserts, @ifc-lite/create provides anchored builders that emit a complete sub-graph (placement, profile, extruded solid, representation, product shape, rel-contained-in-spatial-structure) into the overlay. The same builder backs every Add Element panel chip in the viewer — and the SDK / sandbox bim.store.* namespace.
| Builder | Signature highlights | Profile modes |
|---|---|---|
addColumnToStore |
Position, Width × Depth × Height |
rectangle |
addWallToStore |
Start, End, Thickness, Height (planar XY axis enforced) |
linear |
addBeamToStore |
Start, End, Width × Height cross-section |
linear |
addMemberToStore |
Start, End, Width × Height, PredefinedType |
linear |
addSlabToStore |
Position + Width × Depth × Thickness or OuterCurve polygon |
rectangle / polygon |
addRoofToStore |
same shape as slab; emits .FLAT_ROOF. PredefinedType |
rectangle / polygon |
addPlateToStore |
same shape as slab — thin extruded plate | rectangle / polygon |
addSpaceToStore |
rectangle or polygon footprint, extruded by Height. Aggregated to its storey via IfcRelAggregates |
rectangle / polygon |
addDoorToStore |
Position, Width × Height, optional OperationType + UserDefinedOperationType |
n/a |
addWindowToStore |
Position, Width × Height, optional PartitioningType + UserDefinedPartitioningType |
n/a |
import { StoreEditor } from '@ifc-lite/mutations';
import { addColumnToStore, resolveSpatialAnchor } from '@ifc-lite/create';
const editor = new StoreEditor(dataStore, view);
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
// ↳ walks the parsed store for IfcOwnerHistory, the 'Body' representation
// context, and the storey's IfcLocalPlacement.
const result = addColumnToStore(editor, anchor, {
Position: [1, 1, 0], // storey-local metres
Width: 0.3,
Depth: 0.4,
Height: 3,
Name: 'Column 1',
});
// → { columnId, placementId, profileId, solidId, shapeRepId, productShapeId, relContainedId }
The column lands in the existing spatial hierarchy, references the model's own owner history and 'Body' subcontext, and exports as a set of new STEP entities the next time you call StepExporter.export({ applyMutations: true }). No script + re-parse round-trip needed.
IFC4 vs IFC2X3¶
Builders read the schema from the resolved anchor (anchor.schema) and drop attribute-tail slots that don't exist in IFC2X3. For example IfcWall.PredefinedType and IfcDoor.OperationType are emitted on IFC4 only; on IFC2X3 the corresponding STEP records are 8 / 10 attributes wide. USERDEFINED enums round-trip through their companion User-defined… slot, so a custom OperationType: 'USERDEFINED' + UserDefinedOperationType: 'Sliding-Curve' exports as .USERDEFINED.,'Sliding-Curve'.
Auto Spaces — generate IfcSpace from a storey's walls¶
For room generation, @ifc-lite/create ships a planar-graph face finder that turns a storey's wall axes into a CCW polygon per enclosed region:
import { generateSpacesFromWalls } from '@ifc-lite/create';
const result = generateSpacesFromWalls(editor, dataStore, storeyExpressId, {
snapTolerance: 0.05, // collapse sloppy wall ends within 5 cm
minArea: 0.5, // drop closets / slivers
height: 3, // IfcSpace extrusion in m
namePattern: 'Space {n}',
predefinedType: 'INTERNAL',
// dryRun: true, // detect-only — no IfcSpace emitted
});
// → { wallsConsidered, wallsContributing, detected: DetectedSpace[], emitted: [...] }
The detector also picks up overlay walls (placed via addWallToStore since the model was parsed) when you pass an OverlayWallReader — the viewer wires this in automatically so the Auto Spaces button works on freshly-drawn walls without a re-parse. detectEnclosedAreas(segments, options) is exported as the pure pipeline step if you want detection without IFC emission.
bim.store.* — Scripting & SDK¶
In the viewer's QuickJS sandbox and the TypeScript SDK, the same surface is exposed as bim.store:
// SDK (TypeScript app)
const profile = bim.store.addEntity('arch', {
type: 'IfcRectangleProfileDef',
attributes: ['.AREA.', null, '#34', 0.6, 0.4],
});
bim.store.setPositionalAttribute(profile, 3, 0.7);
bim.store.removeEntity(unwantedRef);
// High-level builder
const storey = bim.query.byType('IfcBuildingStorey')[0].ref.expressId;
const col = bim.store.addColumn('arch', storey, {
Position: [1, 1, 0],
Width: 0.3, Depth: 0.4, Height: 3,
Name: 'Column 1',
});
The sandbox gates bim.store.* behind a store: true permission (default false, mirrors the existing mutate permission). The viewer opts in.
Viewer UI¶
The viewer surfaces store-level edits in three places — see Viewer Integration below for the full UX:
- Raw STEP tab in the properties panel — inline pen-icon editor on every positional argument. Edited rows show a purple dot; the editor parses the same STEP literal conventions as
setPositionalAttribute. The tab also opens for overlay-only entities (freshly added or duplicated) so newly-created walls / columns / spaces are immediately inspectable, even before export. - Right-click → Delete entity — calls
removeEntity, surfaces a toast with undo support. - Right-click on a storey → Add Column here… — opens the Add Column dialog, calls
addColumnon submit, and selects the new column in the 3D scene. - Add Element panel (command palette →
Add elementor shortcut). Right-side panel with chips for every supported type, per-type form, click-to-place flow, and a 3D ghost preview that updates live as you adjust dimensions. Snap-to-vertex/edge/face is on by default (toggle withS); placements off-surface fall back to the storey floor plane so you can drop columns / walls into empty rooms. Picking theSpacechip reveals an Auto Spaces sub-panel that runs the wall-graph face finder with adjustable snap tolerance / min area / height / naming pattern and a Preview button before commit.
All paths route through the same mutationSlice actions that wrap StoreEditor, so undo/redo (Ctrl+Z / Ctrl+Shift+Z) covers store-level edits identically to property edits. Each commit also injects a renderer-frame mesh into the geometry pipeline so the new element appears in 3D the moment the action fires — no export+reparse round-trip required.
When to use what¶
| You want to… | Use |
|---|---|
| Edit an IfcRoot named attribute (Name, FireRating, ObjectType, …) | setProperty / setAttribute (see above) |
| Edit a positional STEP arg on a non-IfcRoot entity (profile dim, cartesian point, …) | setPositionalAttribute / bim.store.setPositionalAttribute |
| Inject a small raw STEP entity (a point, a profile, a unit) | addEntity / bim.store.addEntity |
| Drop a fully-formed building element with geometry | addColumnToStore / addWallToStore / addSlabToStore / addBeamToStore / addDoorToStore / addWindowToStore / addSpaceToStore / addRoofToStore / addPlateToStore / addMemberToStore (or bim.store.add{Column,Wall,Slab,…}) |
| Generate IfcSpace volumes from a storey's existing walls | generateSpacesFromWalls (or Add Element → Space → Auto Spaces in the viewer) |
| Duplicate any IfcRoot product (psets, qsets, materials, type associations preserved) | duplicateInStore / right-click → Duplicate |
| Remove an entity from an existing model | removeEntity / bim.store.removeEntity |
| Build a brand-new IFC file from scratch | IfcCreator (see API Reference) |
Key Types¶
| Type | Description |
|---|---|
MutablePropertyView |
Wraps property table with mutation overlay (properties, quantities, attributes, positional args, new entities, tombstones) |
StoreEditor |
High-level facade for store-level edits — addEntity, removeEntity, setPositionalAttribute |
Mutation |
A single change with old/new values. type is one of UPDATE_PROPERTY, UPDATE_QUANTITY, UPDATE_ATTRIBUTE, UPDATE_POSITIONAL_ATTRIBUTE, CREATE_ENTITY, DELETE_ENTITY, … |
ChangeSet |
Named collection of mutations |
ChangeSetManager |
Manages multiple change sets |
BulkQueryEngine |
Query and update entities in bulk |
CsvConnector |
Import property data from CSV files |
addColumnToStore |
High-level anchored IfcColumn builder (@ifc-lite/create) |
resolveSpatialAnchor |
Walks a parsed store for owner history, 'Body' context, and storey placement (@ifc-lite/create) |