Lists and Schedules¶
IFClite can turn model data into configurable property tables, the BIM equivalent of door schedules and wall schedules. The @ifc-lite/lists package evaluates a list definition against parsed model data and produces rows you can display, group, summarise, or export as CSV.
How It Works¶
A list definition describes what to tabulate:
- Entity types - Which IFC classes to include (e.g. all
IfcDoor) - Columns - Which values to pull for each entity (attributes, properties, quantities, ...)
- Conditions - Optional filters on property values
executeList runs the definition against a data provider (an adapter over your parsed model) and returns a ListResult with one row per matching entity.
Quick Start¶
import { executeList, listResultToCSV, LIST_PRESETS } from '@ifc-lite/lists';
import type { ListDataProvider } from '@ifc-lite/lists';
// LIST_PRESETS[0] is the Wall Schedule
const result = executeList(LIST_PRESETS[0], provider);
console.log(`${result.rows.length} walls`);
// Export as CSV
const csv = listResultToCSV(result);
executeList(definition, provider, modelId?) takes an optional third argument tagging rows with a model id (defaults to 'default'), useful when running the same list across multiple loaded models.
Column Sources¶
Each ColumnDefinition has a source that says where the value comes from:
| Source | Description | Example |
|---|---|---|
attribute |
Direct IFC attribute | Name, GlobalId, ObjectType, Class |
property |
Property from a pset | Pset_WallCommon.FireRating |
quantity |
Quantity from a qset | Qto_WallBaseQuantities.NetSideArea |
material |
Associated material names (joined with ", ") |
Concrete, Insulation |
classification |
Classification references (joined with ", ") |
Uniclass Ss_25_10 |
spatial |
Containing spatial element name; propertyName picks the level (Storey (default), Building, Site, Project) |
Level 2 |
model |
Source file name | office.ifc |
zone |
Location-zone assignment and per-zone volume (user-defined 3D zone boxes, viewer-computed); psetName holds the zone-set id, propertyName picks the mode — see Zone columns |
Section A |
A column looks like:
import type { ColumnDefinition } from '@ifc-lite/lists';
const fireRating: ColumnDefinition = {
id: 'prop-pset_doorcommon-firerating',
source: 'property',
psetName: 'Pset_DoorCommon',
propertyName: 'FireRating',
label: 'FireRating',
};
For quantity columns, psetName holds the quantity set name (e.g. Qto_DoorBaseQuantities).
Zone columns¶
psetName holds the zone-set id (durable; the set's display name is not unique). propertyName is the mode:
| Mode | Cell | Notes |
|---|---|---|
Zone (default) |
Zone name | A straddling element shows every touched zone, joined with ", ". |
Straddles |
Boolean | Whether the element crosses a zone boundary. |
volume (mesh) |
Number | The element's volume inside its HOME zone. Exported as ZONE_MODE_VOLUME. |
volume breakdown (mesh) |
Text | Every zone with a share, as Zone A: 1.2, Zone B: 0.8. Exported as ZONE_MODE_BREAKDOWN. |
Three things about the volume modes are load-bearing:
- The basis is in the mode name.
meshis the as-built geometry, after opening cuts — the only basis whose per-zone split is measured rather than inferred. It is not aNetVolumeand not aGrossVolume; a zone volume derived from a net wall and one derived from a gross wall are not comparable, so the label travels with the number rather than living in a tooltip. The declared net/gross breakdowns are shown side by side in the viewer's properties panel, where there is room for all of them. - They are
nulluntil someone asks. Apportionment is an explicit on-demand action, never part of model load, so adding the column never triggers a model-wide clip. A cell is alsonullwhen the element does not straddle, or when its mesh is not a proven closed solid — in which case no volume may be stated for it at all, let alone split. - The unit is the model's declared volume unit. A
volume (mesh)column is tagged withQuantityType.Volume, so the shared per-column unit resolver converts and labels it exactly like a declaredNetVolume. UseisZoneVolumeMode(mode)rather than comparing strings if you need to make the same decision.
import { ZONE_MODE_VOLUME, type ColumnDefinition } from '@ifc-lite/lists';
// `psetName` is the durable zone-set id, not the set's display name.
const zoneSetId = 'a3f1c0de-...';
const zoneVolume: ColumnDefinition = {
id: 'zone-volume',
source: 'zone',
psetName: zoneSetId,
propertyName: ZONE_MODE_VOLUME,
label: 'Volume in zone',
};
A provider supplies the numbers through one optional method on ListDataProvider:
import type { ListDataProvider } from '@ifc-lite/lists';
type ZoneVolumeShares = NonNullable<ListDataProvider['getZoneVolumeShares']>;
// (expressId: number, zoneSetId: string) => {
// homeValue: number | null;
// shares: Array<{ zoneName: string; value: number }>;
// } | null
Built-in Presets¶
LIST_PRESETS is an array of ready-made ListDefinitions:
| Preset | Entity types | Columns |
|---|---|---|
| Wall Schedule | IfcWall, IfcWallStandardCase | Common properties and base quantities |
| Door Schedule | IfcDoor | FireRating, IsExternal, AcousticRating, Width, Height, Area |
| Window Schedule | IfcWindow | Dimensions |
| Space Areas | IfcSpace | Areas and volumes |
| Zones & Systems | IfcSpatialZone, IfcZone, IfcSystem, IfcDistributionSystem | Names |
| All Elements | Walls, doors, windows, slabs, columns, beams, stairs, roofs, coverings, curtain walls, railings | Overview columns |
Worked Example: Door Schedule to CSV¶
import { executeList, listResultToCSV, LIST_PRESETS } from '@ifc-lite/lists';
// LIST_PRESETS[1] is the Door Schedule:
// Name, Class, ObjectType,
// Pset_DoorCommon.FireRating / IsExternal / AcousticRating,
// Qto_DoorBaseQuantities.Width / Height / Area
const doorSchedule = LIST_PRESETS[1];
const result = executeList(doorSchedule, provider, 'office.ifc');
for (const row of result.rows) {
console.log(row.values);
}
const csv = listResultToCSV(result);
// listResultToCSV(result, delimiter?) - default delimiter is ','
CSV Safety¶
listResultToCSV guards against spreadsheet formula injection (CWE-1236): any cell that starts with =, +, -, @, tab, or carriage return is prefixed with a single quote so Excel and Google Sheets treat it as text rather than a formula. Standard CSV quoting (double quotes, "" escaping) is applied on top.
Grouping, Aggregation & Schedules¶
Set grouping on a ListDefinition to bucket rows by one or more columns (outermost first) and sum numeric columns per group:
import { executeList, summariseListRows, toScheduleRows, LIST_PRESETS } from '@ifc-lite/lists';
const definition = {
...LIST_PRESETS[0], // Wall Schedule
grouping: {
columnId: 'attr-name', // legacy single-column field, kept in sync with columnIds[0]
columnIds: ['attr-name'], // ordered group-by columns, outermost first
sumColumnIds: ['quant-qto_wallbasequantities-length'],
},
};
const result = executeList(definition, provider);
// result.groups is a flat PRE-ORDER list: each parent group is immediately
// followed by its subgroups. Every group carries a Count aggregate (`count`)
// and per-column sums (`sums`).
for (const group of result.groups ?? []) {
console.log(group.label, group.count, group.sums);
}
// A Bonsai-style schedule/pivot table — one row per group-value tuple (the
// LEAF groups only) instead of a nested tree. `levelCount` must be the number
// of grouping columns that were ACTUALLY applied: the engine drops grouping
// ids whose column is no longer in the definition (a stale persisted grouping
// is the common case), so passing the raw `columnIds.length` can ask for a
// deeper leaf level than the groups have — and `toScheduleRows` then matches
// nothing and returns no rows.
const activeGroupIds = definition.grouping.columnIds.filter(
(id) => definition.columns.some((c) => c.id === id),
);
const scheduleRows = toScheduleRows(result.groups, activeGroupIds.length);
for (const row of scheduleRows) {
console.log(row.path, row.count, row.sums); // e.g. ["Wall-01"], 1, { ... }
}
summariseListRows(definition, rows) is what executeList calls internally to build groups/summary; call it directly when you already have rows from elsewhere (e.g. merged across federated models) and just need to re-derive the grouping.
The Data Provider¶
executeList reads model data through the ListDataProvider interface, so the package has no hard dependency on how you parsed the model. Required methods include getEntitiesByType, getEntityName, getEntityGlobalId, getPropertySets, and getQuantitySets; optional methods (getMaterialNames, getClassifications, getStoreyName, getProjectName, getZoneAssignment, getZoneSetNames, getZoneVolumeShares, ...) unlock the material, classification, spatial, model, and zone column sources, and the engine degrades gracefully when they are absent (a zone column simply resolves to null on a provider without zone data). The two volume modes go through getZoneVolumeShares specifically, so a provider that implements getZoneAssignment but not getZoneVolumeShares still answers Zone and Straddles and returns null for the volumes.
Discovering Columns¶
To build a column picker UI (or just see what a model contains), use discoverColumns:
import { discoverColumns } from '@ifc-lite/lists';
import { IfcTypeEnum } from '@ifc-lite/data';
// Accepts one provider or an array of providers
const discovered = discoverColumns(provider, [IfcTypeEnum.IfcDoor]);
discovered.attributes; // available entity attributes
discovered.properties; // Map<psetName, propertyNames[]>
discovered.quantities; // Map<qsetName, quantityNames[]>
It samples up to 50 entities per type per provider, so it stays fast on large models.
Name Patterns¶
Conditions and lookups that match by name accept either an exact string or a regex literal. compileNameMatcher(pattern) returns a (name: string) => boolean:
/fire.*rating/i- a/body/flagsstring compiles to a regular expression- anything else - exact, case-sensitive match
isNamePattern(pattern) tells you whether a string will be treated as a regex.
Key Exports¶
| Export | Description |
|---|---|
executeList(definition, provider, modelId?) |
Run a list definition, returns ListResult |
listResultToCSV(result, delimiter?) |
CSV export with formula-injection guard |
summariseListRows |
Aggregate rows into group summaries (ListGroup[] + whole-result ListSummary) |
groupingColumnIds(grouping) |
Resolve a grouping config's ordered group-by column ids |
toScheduleRows(groups, levelCount) |
Project grouped ListGroup[] to a schedule/pivot ListScheduleRow[] — one row per group-value tuple |
discoverColumns(providers, entityTypes) |
Sample available attributes/properties/quantities |
compileNameMatcher(pattern) / isNamePattern(pattern) |
Exact-or-regex name matching |
LIST_PRESETS |
Built-in schedule definitions |
ENTITY_ATTRIBUTES |
The attribute names available to attribute columns |
See the package README and the type definitions (ListDefinition, ColumnDefinition, PropertyCondition, ListDataProvider) for the full API.