Skip to content

Parsing IFC Files

Complete guide to parsing IFC files with IFClite, covering both IFC4 (STEP) and IFC5 (IFCX) formats.

Overview

IFClite supports the columnar STEP parser for IFC4-style files and the IFCX parser for IFC5 JSON files:

Aspect Options
Processing Client-side (WASM) or Server-side (Rust)
Format IFC4 (STEP) or IFC5 (IFCX JSON)
Mode Columnar or streaming
flowchart TB subgraph Formats["File Formats"] IFC4["IFC4 (STEP)"] IFC5["IFC5 (IFCX JSON)"] end subgraph Detect["Format Detection"] Auto["parseAuto()"] end subgraph Parsers["Parsers"] Parser["@ifc-lite/parser"] IFCX["@ifc-lite/ifcx"] Server["Server API"] end IFC4 --> Auto IFC5 --> Auto Auto --> Parser Auto --> IFCX IFC4 --> Server style IFC4 fill:#6366f1,stroke:#312e81,color:#fff style IFC5 fill:#10b981,stroke:#064e3b,color:#fff style Auto fill:#f59e0b,stroke:#7c2d12,color:#fff

Client-Side Parsing

Basic Parsing

import { IfcParser } from '@ifc-lite/parser';

const parser = new IfcParser();
const buffer = await fetch('model.ifc').then(r => r.arrayBuffer());

// Columnar parse (returns IfcDataStore - recommended for all STEP files)
const store = await parser.parseColumnar(buffer);

parseColumnar() is the canonical IFC STEP path. It centralizes scan selection across pre-scanned indexes, the browser worker scanner, the WASM byte scanner, and the TypeScript tokenizer fallback, then builds an IfcDataStore for on-demand extraction. parse() remains as a compatibility adapter for callers that still need the older eager ParseResult shape.

The columnar parser returns an IfcDataStore with memory-efficient data structures:

const store = await parser.parseColumnar(buffer, {
  onProgress: ({ phase, percent }) => {
    console.log(`${phase}: ${percent}%`);
  }
});

// Access entities by type
const wallIds = store.entityIndex.byType.get('IFCWALL') ?? [];
const doorIds = store.entityIndex.byType.get('IFCDOOR') ?? [];

// Access entity by ID
const entityRef = store.entityIndex.byId.get(123);

// Metadata
console.log(`Schema: ${store.schemaVersion}`);  // IFC2X3, IFC4, IFC4X3, IFC5
console.log(`Entities: ${store.entityCount}`);
console.log(`Parse time: ${store.parseTime}ms`);

Browser Worker Mode

For non-blocking parsing in the browser, WorkerParser runs the columnar parser in a Web Worker. It takes a SharedArrayBuffer (so the same bytes can also be handed to the geometry workers without a copy), which requires a cross-origin-isolated page:

import { WorkerParser } from '@ifc-lite/parser/browser';

if (WorkerParser.isSupported()) {
  // Copy the file bytes into a SharedArrayBuffer
  const sab = new SharedArrayBuffer(buffer.byteLength);
  new Uint8Array(sab).set(new Uint8Array(buffer));

  const parser = new WorkerParser();
  const controller = new AbortController();
  try {
    const store = await parser.parseColumnar(sab, {
      signal: controller.signal,
      onProgress: ({ phase, percent }) => {
        // Updates from worker thread
        updateProgressUI(phase, percent);
      }
    });
  } catch (err) {
    if (err instanceof Error && err.name === 'AbortError') {
      // Cancelled — see below.
    } else {
      throw err;
    }
  }
  // The worker self-terminates after each parse. To cancel an in-flight
  // parse early, either abort `controller` or call `parser.terminate()`;
  // both terminate the worker and reject the pending promise, so `await`
  // never hangs. `controller.abort()` rejects with `signal.reason`: an
  // `AbortError` by default, but a custom `controller.abort(reason)` rejects
  // with that reason instead, so the `AbortError` check above won't match it.
  // `parser.terminate()` always rejects with an `AbortError`. Each
  // `parseColumnar` call has its own worker: aborting one call's signal
  // cancels only that parse, while `terminate()` cancels every in-flight parse.
} else {
  // Fall back to the in-process parser (no SAB / not cross-origin isolated)
  const store = await new IfcParser().parseColumnar(buffer);
}

For an integrated geometry/parser load, both WorkerParser.parseColumnar and GeometryProcessor.processAdaptive accept an optional sourceFingerprint cell. Use a fresh 16-byte SharedArrayBuffer for each immutable source and pass that same cell to both calls. Its four unsigned 32-bit words hold source length low and high halves, hash, and readiness; initialize only the two length words before starting either call. Never reuse a cell for another source or load. The existing prepass worker computes the full-source key when its WASM supports that operation; the parser uses it only if ready, otherwise it computes the same key itself without waiting. Omitting the cell retains ordinary parsing behavior. This optimization does not change partial/final source accessor identity or cache keys.

Streaming Geometry

For large files, stream geometry progressively using GeometryProcessor.processStreaming():

import { GeometryProcessor } from '@ifc-lite/geometry';

const parser = new IfcParser();
const geometry = new GeometryProcessor();
await geometry.init();

// Parse first (fast, metadata only)
const store = await parser.parseColumnar(buffer);

// Stream geometry progressively
for await (const event of geometry.processStreaming(new Uint8Array(buffer))) {
  switch (event.type) {
    case 'start':
      console.log(`Starting geometry extraction`);
      break;
    case 'batch':
      // Add meshes to renderer as they arrive
      renderer.addMeshes(event.meshes, true);  // isStreaming = true
      progressBar.value = event.totalSoFar;  // cumulative mesh count (no percentage on batch events)
      break;
    case 'complete':
      console.log(`Done: ${event.totalMeshes} meshes`);
      renderer.fitToView();
      break;
  }
}

On-Demand Property Extraction

Properties and quantities are extracted lazily for better performance with large files:

import {
  extractPropertiesOnDemand,
  extractQuantitiesOnDemand,
  extractEntityAttributesOnDemand
} from '@ifc-lite/parser';

// Parse without pre-loading properties
const store = await parser.parseColumnar(buffer);

// Extract properties only when needed
const wallId = wallIds[0];
const psets = extractPropertiesOnDemand(store, wallId);

for (const pset of psets) {
  console.log(`Property Set: ${pset.name}`);
  for (const prop of pset.properties) {
    console.log(`  ${prop.name}: ${prop.value}`);
  }
}

// Extract quantities
const qsets = extractQuantitiesOnDemand(store, wallId);
for (const qset of qsets) {
  console.log(`Quantity Set: ${qset.name}`);
  for (const qty of qset.quantities) {
    console.log(`  ${qty.name}: ${qty.value} ${qty.type}`);
  }
}

// Extract IFC attributes
const attrs = extractEntityAttributesOnDemand(store, wallId);
console.log(`Name: ${attrs.name}`);
console.log(`GlobalId: ${attrs.globalId}`);
console.log(`Description: ${attrs.description}`);

Type-Inherited Properties

extractPropertiesOnDemand returns only the sets attached to the occurrence itself. Properties defined once on the element's IfcTypeProduct — where Pset_*Common usually lives — come from extractTypePropertiesOnDemand.

Combine them with mergeInheritedPropertySets. IFC inherits per property, not per property set: an occurrence and its type routinely both carry a set of the same name holding different properties, so replacing the whole set on a name collision would hide every type-only property in it.

import {
  extractPropertiesOnDemand,
  extractTypePropertiesOnDemand,
  mergeInheritedPropertySets
} from '@ifc-lite/parser';

const own = extractPropertiesOnDemand(store, wallId);
const inherited = extractTypePropertiesOnDemand(store, wallId)?.properties ?? [];

const psets = mergeInheritedPropertySets(own, inherited);
  • Where both sides define the same property name, the occurrence wins — it is the more specific definition.
  • An inherited set whose name the occurrence does not use is appended as-is.
  • If the occurrence carries several sets of one name (one per IfcRelDefinesByProperties), the inherited properties are merged into every one of them.
  • Neither argument is mutated, so cached extractor results stay reusable.

How On-Demand Works

sequenceDiagram participant App participant Parser participant Store participant Source App->>Parser: parseColumnar(buffer) Parser->>Store: Build entity index Parser->>Store: Build on-demand maps Note over Store: entityId -> [psetIds] Parser-->>App: IfcDataStore (no properties yet) App->>Store: extractPropertiesOnDemand(wallId) Store->>Store: Lookup psetIds for wallId Store->>Source: Parse pset entities from buffer Store-->>App: PropertySet[]

IFC5 (IFCX) Parsing

IFClite natively supports the new IFC5 JSON-based format with ECS composition and USD geometry.

Format Detection

import { parseAuto } from '@ifc-lite/parser';
import { detectFormat } from '@ifc-lite/ifcx';

// Auto-detect and parse
const result = await parseAuto(buffer);

if (result.format === 'ifcx') {
  // IFC5 file: parsed data lives under result.data, meshes at the top level
  const { entities, spatialHierarchy } = result.data;
  const meshes = result.meshes;
} else {
  // IFC4 STEP file: the IfcDataStore is result.data
  const store = result.data;
}

// Or detect format manually
const format = detectFormat(buffer);  // 'ifc', 'ifcx', 'glb', or 'unknown'

.ifcZIP Containers

parseAuto (and every built-in loader — CLI, MCP, the viewer) transparently unwraps the buildingSMART .ifcZIP container format: a zip archive wrapping a single .ifc/.ifcxml file. Feed it the zip bytes directly — no manual unzip step needed:

import { parseAuto, unwrapIfcZip } from '@ifc-lite/parser';

// parseAuto detects and unwraps .ifcZIP automatically
const result = await parseAuto(zipBuffer);

// Or unwrap explicitly (a no-op for a non-zip buffer)
const ifcBuffer = await unwrapIfcZip(zipBuffer);

unwrapIfcZip returns only model bytes. For textured archives, call unwrapIfcZipWithResources: its resources map resolves PNG/JPEG images by lowercased basename (first entry wins), while originalResources preserves each archive path using the same byte arrays. modelPath identifies the IFC entry; keep that path and the original image paths when repackaging a model whose texture references are relative. Non-zip input returns empty resource maps and no modelPath. Image extraction applies per-entry and aggregate size/count limits. resourcesIncomplete is true when those budgets omit at least one image; a portable exporter must report this rather than claim all resources were preserved. Other resource formats are not extracted.

An archive with zero or more than one .ifc/.ifcxml entry throws rather than guessing which one to load.

Direct IFCX Parsing

import { parseIfcx } from '@ifc-lite/ifcx';

const result = await parseIfcx(buffer, {
  onProgress: ({ phase, percent }) => {
    console.log(`${phase}: ${percent}%`);
  }
});

// IFC5 uses ECS (Entity-Component-System) composition
console.log(`Entities: ${result.entityCount}`);
console.log(`Meshes: ${result.meshes.length}`);

// Pre-tessellated USD geometry
for (const mesh of result.meshes) {
  console.log(`Entity #${mesh.expressId}: ${mesh.ifcType}`);
  // mesh.positions, mesh.normals, mesh.indices ready for GPU
}

// Same data structures as IFC4
console.log(`Schema: ${result.schemaVersion}`);  // 'IFC5'
console.log(`Parse time: ${result.parseTime}ms`);

IFC5 Features

Feature Description
ECS Composition Entities composed from components (attributes)
USD Geometry Pre-tessellated meshes (no WASM triangulation needed)
Layer Semantics Multiple nodes at same path merge (later overrides)
Namespace Attributes Properties prefixed with namespace (e.g., bsi::ifc::prop::)
JSON Format Human-readable, streamable

IFC5 Data Model

parseIfcx returns the same columnar tables as the STEP parser (EntityTable, PropertyTable, QuantityTable, RelationshipGraph, SpatialHierarchy), plus IFCX-specific path mappings:

import { parseIfcx } from '@ifc-lite/ifcx';

const result = await parseIfcx(buffer);

// Columnar entity table
const { entities } = result;
for (const id of entities.expressId) {
  console.log(`Entity #${id}: ${entities.getTypeName(id)}`);
  console.log(`  GlobalId: ${entities.getGlobalId(id)}`);
  console.log(`  Name: ${entities.getName(id)}`);
  console.log(`  Has geometry: ${entities.hasGeometry(id)}`);
}

// Pick an element to inspect (first IfcWall in the table)
const wallId = result.entities.expressId.find(
  (id) => result.entities.getTypeName(id) === 'IfcWall',
)!;

// Property sets for an element (namespace-prefixed names)
for (const pset of result.properties.getForEntity(wallId)) {
  console.log(`PropertySet: ${pset.name}`);
  for (const prop of pset.properties) {
    console.log(`  ${prop.name}: ${prop.value}`);
  }
}

// Spatial hierarchy
const hierarchy = result.spatialHierarchy;
console.log(`Project: ${hierarchy.project.name}`);

// Element-to-storey lookup
const storeyId = hierarchy.elementToStorey.get(wallId);

// IFCX path <-> express ID mappings
const path = result.idToPath.get(wallId);
const id = result.pathToId.get(path);

spatialHierarchy.getContainingSpace(elementId) reads the live canonical containment index. It resolves a directly contained element or aggregated descendant to its nearest containing space, and reflects authored containment changes and Undo without rebuilding the hierarchy.

Server-Side Parsing

For production deployments, use the server for parallel processing and caching:

import { IfcServerClient } from '@ifc-lite/server-client';

const client = new IfcServerClient({
  baseUrl: 'http://localhost:3001'
});

// Parquet format (15x smaller than JSON)
const result = await client.parseParquet(file);

// Streaming for large files (onBatch callback fires per geometry batch)
await client.parseParquetStream(file, (batch) => {
  // batch.meshes are server MeshData (snake_case fields like express_id);
  // map them to your renderer's mesh format before uploading.
  console.log(`Batch ${batch.batch_number}: ${batch.meshes.length} meshes`);
});

See the Server Guide for complete server documentation.

Native Rust decoder scratch buffers

For repeated native decoding, caller-owned scratch vectors can be reused with EntityDecoder::get_entity_ref_list_fast_into(entity_id, &mut ids) and get_polyloop_coords_cached_into(entity_id, &mut coords). Each replaces the buffer contents and returns Some(()) on success or None with an empty buffer on failure. The reference-list helper preserves authored order and duplicates, dropping oversized references. The PolyLoop helper preserves coordinate order and rejects the whole loop for missing or oversized point references; points already resolved remain in the decoder's existing point cache even on failure. Both retain the behavior of their allocating convenience accessors. The caller controls scratch-vector lifetime; reuse does not promise physical memory release.

Parse Options

interface ParseOptions {
  // Progress callback
  onProgress?: (progress: { phase: string; percent: number }) => void;

  // Diagnostic message callback
  onDiagnostic?: (message: string) => void;

  // Optional IfcAPI instance for WASM-accelerated entity scanning
  wasmApi?: WasmScanApi;

  // Yield budget for large incremental parses (higher finishes faster with longer main-thread slices)
  yieldIntervalMs?: number;

  // Keep property-set containers indexed but defer individual property/quantity atoms
  deferPropertyAtomIndex?: boolean;

  // Skip worker-based entity scanning and stay in-process
  disableWorkerScan?: boolean;

  // Called when spatial hierarchy is ready, before property/association parsing completes
  onSpatialReady?: (partialStore: IfcDataStore) => void;

  // Pre-built entity index from another worker (e.g. the streaming geometry pre-pass)
  preScannedEntityIndex?: PreScannedEntityIndex;
}

const store = await parser.parseColumnar(buffer, {
  deferPropertyAtomIndex: true,
  onProgress: ({ phase, percent }) => console.log(`${phase}: ${percent}%`)
});

Tessellation and geometry quality are configured on the GeometryProcessor, not on parseColumnar().

IfcDataStore Structure

interface IfcDataStore {
  // Metadata
  fileSize: number;
  schemaVersion: 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
  entityCount: number;
  parseTime: number;

  // Raw source (for on-demand parsing)
  source: Uint8Array;

  // Entity index
  entityIndex: {
    byId: Map<number, EntityRef>;      // expressId -> EntityRef
    byType: Map<string, number[]>;     // type -> [expressId, ...]
  };

  // Columnar tables
  strings: StringTable;                 // Deduplicated strings
  entities: EntityTable;                // Entity metadata
  properties: PropertyTable;            // Pre-computed (or empty for on-demand)
  quantities: QuantityTable;            // Pre-computed (or empty for on-demand)
  relationships: RelationshipGraph;     // Relationship edges

  // Spatial structures
  spatialHierarchy?: SpatialHierarchy;
  spatialIndex?: SpatialIndex;

  // On-demand maps
  onDemandPropertyMap?: Map<number, number[]>;   // entityId -> [psetId, ...]
  onDemandQuantityMap?: Map<number, number[]>;   // entityId -> [qsetId, ...]
}

Source-empty server stores can provide resolvedClassifications, an optional map from entity express IDs to ClassificationInfo[]. extractClassificationsOnDemand combines the entity's rows with those inherited through IfcRelDefinesByType, using the relationship graph to confirm classification associations. The server derives classifications and relationships from the same immutable IFC input. Repeated relationships may produce multiple resolved rows for one deduplicated graph edge. When resolved rows are absent, the parser returns unresolved markers instead of certifying classification attributes. Source-bearing stores continue to decode classifications from their IFC bytes.

Native Rust owned index columns

ColumnarEntityIndex::from_owned_columns(ids, starts, lengths) consumes three Vec<u32> columns. Like the borrowed from_columns constructor, it sorts when needed, keeps the last input occurrence of duplicate IDs, and returns an empty index for empty or mismatched columns. Already sorted, unique columns retain their existing allocations. Use from_columns when the caller must keep owning its input vectors.

Spatial Hierarchy

import { IfcTypeEnum, type SpatialNode } from '@ifc-lite/data';

// spatialHierarchy is optional on IfcDataStore; guard before use
const hierarchy = store.spatialHierarchy;
if (!hierarchy) throw new Error('No spatial hierarchy in this model');

// Project structure
console.log(`Project: ${hierarchy.project.name}`);

// Storeys are NOT direct children of the project: the tree is
// Project -> Site -> Building -> Storey, so walk it rather than reading
// `project.children` directly.
function* storeysOf(node: SpatialNode): Generator<SpatialNode> {
  if (node.type === IfcTypeEnum.IfcBuildingStorey) yield node;
  for (const child of node.children ?? []) yield* storeysOf(child);
}

// Navigate storeys (SpatialNode.type is a numeric IfcTypeEnum)
for (const storey of storeysOf(hierarchy.project)) {
  console.log(`Storey: ${storey.name}`);

  // Get elements on this storey (byStorey is keyed by the storey express id)
  const elements = hierarchy.byStorey.get(storey.expressId) ?? [];
  console.log(`  Elements: ${elements.length}`);

  // Get storey elevation
  const elevation = hierarchy.storeyElevations.get(storey.expressId);
  console.log(`  Elevation: ${elevation}m`);
}

// Find storey for an element
const storeyId = hierarchy.elementToStorey.get(wallId);

Schema Support

IFC2X3, IFC4 and IFC4X3 are parsed with the same pipeline; the runtime schema registry itself is generated from IFC4. IFC5 (IFCX) support is a separate, beta code path (@ifc-lite/ifcx) not covered by the entity tables below.

Per-entity support for IFC2X3/IFC4/IFC4X3 — whether each concrete class is in the schema registry at all, resolves to a real internal type instead of being dropped, is routed to a geometry processor, can be created by @ifc-lite/create, and converts across schema versions — is generated directly from source, not hand-maintained, in the coverage ledger.

Native Rust geometry classification

ifc_lite_core::geometry_flags_by_name(type_name) returns (has_geometry, is_representationless_spatial_container), using the same case normalization and legacy-type rules as the two individual predicates. These flags classify a type; they do not establish whether a particular record contains a usable representation.

Schema Registry

Access runtime schema metadata (generated from IFC4_ADD2_TC1):

import {
  SCHEMA_REGISTRY,
  getEntityMetadata,
  getAllAttributesForEntity,
  isKnownEntity
} from '@ifc-lite/parser';

// Check if entity type is known
if (isKnownEntity('IFCWALL')) {
  const meta = getEntityMetadata('IFCWALL');
  console.log(`Parent: ${meta.parent}`);         // 'IfcBuildingElement'
  console.log(`Abstract: ${meta.isAbstract}`);   // false

  // Get all attributes including inherited
  const attrs = getAllAttributesForEntity('IFCWALL');
  for (const attr of attrs) {
    console.log(`${attr.name}: ${attr.type}`);
  }
}

Advanced Extractors

Materials

import {
  extractMaterials,
  getMaterialForElement,
  getMaterialNameForElement
} from '@ifc-lite/parser';

// The batch extractors read the legacy Map representation from a
// ParseResult (`parser.parse`), so pass its entity maps, not the store.
const materials = extractMaterials(parseResult.entities, parseResult.entityIndex.byType);

// Get material for an element
// getMaterialForElement returns a material express id (or undefined)
const materialId = getMaterialForElement(wallId, materials);
if (materialId !== undefined) {
  const material = materials.materials.get(materialId);
  if (material) {
    console.log(`Material: ${material.name}`);
  }

  // Layered materials resolve to a MaterialLayerSet of layer ids
  const layerSet = materials.materialLayerSets.get(materialId);
  if (layerSet) {
    for (const layerId of layerSet.layers) {
      const layer = materials.materialLayers.get(layerId);
      if (!layer) continue;
      const layerMat = materials.materials.get(layer.material);
      // layer.thickness is in the file's length unit
      console.log(`  Layer: ${layerMat?.name ?? layer.material} (${layer.thickness})`);
    }
  }
}

Georeferencing

import {
  extractGeoreferencing,
  transformToWorld,
  transformToLocal
} from '@ifc-lite/parser';

// Pass a ParseResult's entity maps (`parser.parse`), not the columnar store.
const georef = extractGeoreferencing(parseResult.entities, parseResult.entityIndex.byType);

if (georef) {
  console.log(`CRS: ${georef.projectedCRS?.name}`);
  console.log(`Eastings: ${georef.mapConversion?.eastings}`);
  console.log(`Northings: ${georef.mapConversion?.northings}`);

  // Transform a local coordinate (tuple) to world; returns a tuple or null
  const world = transformToWorld([10, 20, 0], georef);
  if (world) {
    console.log(`World: ${world[0]}, ${world[1]}, ${world[2]}`);
  }
}

Classifications

import {
  extractClassifications,
  getClassificationsForElement,
  groupElementsByClassification
} from '@ifc-lite/parser';

// Pass a ParseResult's entity maps (`parser.parse`), not the columnar store.
const classifications = extractClassifications(parseResult.entities, parseResult.entityIndex.byType);

// Get classifications for an element
const codes = getClassificationsForElement(wallId, classifications);
for (const code of codes) {
  console.log(`${code.identification} - ${code.name}`);
  // e.g., "Pr_60_10_32 - External walls" (owning system via code.referencedSource)
}

// Group elements by classification
const groups = groupElementsByClassification(classifications);
groups.forEach((elementIds, code) => {
  console.log(`${code}: ${elementIds.length} elements`);
});

Error Handling

import { IfcParser } from '@ifc-lite/parser';

const parser = new IfcParser();

try {
  const store = await parser.parseColumnar(buffer);
} catch (error) {
  // parseColumnar throws standard Error instances on malformed STEP syntax,
  // unknown or unsupported schemas, and other parse failures.
  if (error instanceof Error) {
    console.error(`Parse failed: ${error.message}`);
  } else {
    throw error;
  }
}

Performance Comparison

Mode Use Case Memory Speed
parse() Small files, full object access High Moderate
parseColumnar() Most use cases Low Fast
GeometryProcessor.processStreaming() Large files (>50MB) Very Low Progressive
Server Production, caching Server-side Fastest (cached)

Performance Tips

  1. Use columnar parsing - parseColumnar() for best memory efficiency
  2. Use on-demand properties - Don't pre-load all properties
  3. Use workers - Import from @ifc-lite/parser/browser for non-blocking
  4. Use server for large files - Parallel processing and caching
  5. Filter entity types - Exclude IFCSPACE, IFCOPENINGELEMENT if not needed

Multi-Model Loading

When working with multiple IFC files (e.g., architectural, structural, and MEP models), use the federation system to load and coordinate them with unified selection and visibility. Each model receives an ID offset to prevent express ID collisions across files. See the Federation Guide for details on multi-model loading, global ID resolution, and coordinated visibility control.

Next Steps

Borrowing compact entity columns

CompactEntityIndex.getColumns() exposes the four numeric backing arrays (expressIds, byteOffsets, byteLengths, typeIndices) and a copy of the typeStrings list. This supports column-aware consumers such as binary cache serialization without creating a reference object for every entity.

The numeric arrays are borrowed and must not be mutated. They remain valid until their owner detaches them. A transport may transfer the arrays when retiring the owning index; cache consumers must not detach them. Changing the returned string list does not change the index. Generic map-compatible indexes remain supported by the parser and cache interfaces.

Parsed, worker-hydrated and server-loaded spatial hierarchies use the shared spatialLookups helper from @ifc-lite/data. getPath accepts both a spatial node and a contained object; containing-space queries follow live membership.

Stores reconstructed without STEP resource rows can supply immutable IfcDataStore.georeferencing (GeoreferenceInfo or null) alongside lengthUnitScale. extractGeoreferencingOnDemand uses this pre-extracted fact before scanning STEP bytes. An absent field retains ordinary source extraction; null explicitly records that no georeference was supplied. The worker transport preserves these fields when reconstructing a store.

computeTransformMatrix(MapConversion) derives the canonical 4×4 matrix from the current conversion, including its optional axis scale factors.

The classification readers accept an optional native mutation view: extractClassificationsOnDemand(store, entityId, mutationView) and extractClassificationSystemsOnDemand(store, mutationView). Definitions, references and association/type memberships then use the effective IFC records, with the same named/positional attribute precedence as export. Model-local caches refresh at the overlay revision. Source-empty stores retain unresolved markers for source attributes they cannot reconstruct; complete authored records remain readable. Source association endpoint edits require the original source bytes: forwarded immutable memberships cannot reconstruct those live edits, so callers must refuse population claims for that case. An unresolved classification chain does not establish that an explicitly named system is absent.

Material occurrence precedence

extractAllMaterialsOnDemand(store, entityId, view) returns every occurrence assignment, falling back to the entity's type only when the occurrence has none. The optional native mutation view applies current relationship retargeting, deletions, aliases, material edits and newly authored assignment shapes through the same effective readers used for STEP export.

extractMaterialPropertiesOnDemand(store, entityId, view, revision) uses the same current assignments for generic material property groups. Named edits apply before positional edits, matching exported records. An unset layer IsVentilated remains undefined rather than becoming an explicit false value.

materialAssignmentsAvailable(store, entityId, view) reports whether the supplied graph and current edits can prove the complete selected material membership. Source-empty transport without membership data, or with relevant edits to unavailable source relationship records, yields false; retained source markers and authored records do not establish a complete count. The Assistant reports null totals and unverified source markers in those cases.

Current inherited type quantities

extractTypeQuantitiesOnDemand(store, expressId) reads the parsed source snapshot. Pass a native mutation view as the optional third argument to follow current IfcRelDefinesByType assignments and native type quantity attribute edits. The shared collector retains its existing quantity type, unit and numeric rules. Consumers still append type quantities after occurrence quantities so their own declared bases take precedence. Extraction does not write, export, or recompute geometry. Source-free stores retain the existing prebuilt quantity-table path in their consumers.

import { extractTypeQuantitiesOnDemand, type IfcDataStore } from '@ifc-lite/parser';
import type { MutablePropertyView } from '@ifc-lite/mutations';

function currentTypeQuantities(store: IfcDataStore, expressId: number, view: MutablePropertyView) {
  return extractTypeQuantitiesOnDemand(store, expressId, view)?.quantities ?? null;
}

For current reads that need completeness, readCurrentTypeQuantities(store, expressId, view) returns { status, reason, value }. Unavailable coverage means current inherited facts remain unknown, with an explicit reason. Keep occurrence quantities separate and preserve that coverage instead of displaying a verified empty type inventory or retrying the source snapshot. The current reader bounds relationship references, type definitions and quantity members; unsupported quantity classes or unreadable values also produce unavailable coverage. Explicit quantity units use the same canonical resolver with current native records, including newly allocated unit entities. Unreadable/deleted/unsupported units, cycles, or more than 512 unit dependency reads refuse current coverage instead of falling back to an obsolete scale or default SI. Source-only reads retain their existing unit conventions.

import { readCurrentTypeQuantities, type IfcDataStore } from '@ifc-lite/parser';
import type { MutablePropertyView } from '@ifc-lite/mutations';

function verifiedCurrentTypeQuantities(store: IfcDataStore, expressId: number, view: MutablePropertyView) {
  const result = readCurrentTypeQuantities(store, expressId, view);
  return { status: result.status, reason: result.reason, quantities: result.value?.quantities ?? null };
}

When a quantity's Unit is unset, its raw value uses the current project's UnitsInContext. readCurrentProjectUnits(store, view, projectId?) follows the current project, unit assignment and canonical unit dependencies. The optional project id selects an owning project; omission keeps the canonical default-project convention. Available results hold a ProjectUnits resolver. Unreadable, deleted, unsupported, cyclic or oversized contexts yield value: null and an explicit unavailable reason. Preserve that coverage instead of substituting the source snapshot or interpreting a raw occurrence quantity as SI. These current reads bound the project inventory and allow at most 512 dependency reads. They do not change source-only conventions, annotate implicit quantities as explicit units, or reinterpret cached geometry.

Unavailable project context leaves implicit property and quantity values raw, without a physical suffix or display-unit conversion. Dimensionless rows remain readable. A quantity with an independently resolved explicit native Unit retains its own symbol and canonical SI scale, including display overrides. Current view records are consulted even when the store has no source buffer. These display rules do not provide live occurrence-member Unit dependency reads; that separate limitation is tracked in #7379.

import { readCurrentProjectUnits, type IfcDataStore } from '@ifc-lite/parser';
import type { MutablePropertyView } from '@ifc-lite/mutations';

function currentVolumeContext(store: IfcDataStore, view: MutablePropertyView) {
  const current = readCurrentProjectUnits(store, view);
  return { status: current.status, reason: current.reason,
    volumeUnit: current.value?.resolvedForUnitType('VOLUMEUNIT') ?? null };
}

findSourceProjectLengthUnit and normalizeMapUnitName expose the STEP writer's existing replacement-unit eligibility to canonical reader consumers. The resolver uses the first effective source IfcProject, its source UnitsInContext, and source assigned length units. Deleted units, unassigned units, overlay-created units, and unsupported labels cannot supply a source replacement reference. Labels compare whole canonical unit names rather than substrings; the physical scale is retained.

This lower layer shares the existing STEP writer resolver without changing its accepted replacement units. Type quantity journal projection consumes it in the subsequent fix for #7355.

Pass current effective IfcProject ids as projectIds, excluding deleted or retyped projects with the canonical iterateEffectiveEntities inventory. The isDeleted callback filters assignment and unit references; it does not filter the project inventory supplied by the caller.