Skip to content

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. mesh is the as-built geometry, after opening cuts — the only basis whose per-zone split is measured rather than inferred. It is not a NetVolume and not a GrossVolume; 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 null until 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 also null when 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 with QuantityType.Volume, so the shared per-column unit resolver converts and labels it exactly like a declared NetVolume. Use isZoneVolumeMode(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/flags string 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.