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 |
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.
Columnar Parsing (Recommended)¶
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¶
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¶
- Use columnar parsing -
parseColumnar()for best memory efficiency - Use on-demand properties - Don't pre-load all properties
- Use workers - Import from
@ifc-lite/parser/browserfor non-blocking - Use server for large files - Parallel processing and caching
- 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¶
- Server Guide - Server-based parsing with caching
- Geometry Guide - Process geometry
- Query Guide - Query parsed data
- Federation Guide - Load and coordinate multiple models
- API Reference - Complete API docs
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.