npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ifc-lite/mutations

v3.2.0

Published

Mutation tracking and property editing for IFC-Lite

Readme

@ifc-lite/mutations

Property editing and mutation tracking for IFClite. Edit IFC properties, quantities, and attributes in-place via an overlay pattern — original data stays read-only, changes export back to STEP. Supports undo / redo, change-set sharing, bulk updates, and CSV import.

MutablePropertyView.getQuantityMutation(entityId, qsetName, quantityName) reads the current quantity override, including unitRemoved. It returns undefined when no override exists. Use this live state when retaining a source unit: an explicit removal must stay removed, including after an edit which omits its unit. The append-only mutation journal is unsuitable for current-state reads after Undo or skipHistory writes.

Installation

npm install @ifc-lite/mutations

Edit a property

Property edits

import { MutablePropertyView } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';

const view = new MutablePropertyView(store.properties, 'arch-model');

const mutation = view.setProperty(
  wallExpressId,
  'Pset_WallCommon',
  'FireRating',
  'REI 120',
  PropertyValueType.Label,
);

console.log(`${mutation.oldValue} → ${mutation.newValue}`);

// Reads return the new value transparently
view.getPropertyValue(wallExpressId, 'Pset_WallCommon', 'FireRating'); // 'REI 120'

Store-level edits

For raw STEP edits — adding entities, deleting them, overriding positional arguments on entities without symbolic attribute names — pair the view with a StoreEditor:

import { MutablePropertyView, StoreEditor } from '@ifc-lite/mutations';

const view = new MutablePropertyView(propertyTable, modelId);
const editor = new StoreEditor(dataStore, view);

// Add a fresh entity (e.g. an IfcRectangleProfileDef)
const profile = editor.addEntity('IfcRectangleProfileDef', [
  '.AREA.', null, '#34', 0.6, 0.4,
]);

// Override a single positional STEP arg by index (zero-based)
editor.setPositionalAttribute(profile.expressId, 3, 0.7);  // XDim → 0.7

// Tombstone an entity
editor.removeEntity(unwantedExpressId);

Edits accumulate in the same overlay used by setProperty / setAttribute and materialise the next time you call StepExporter.export({ applyMutations: true }).

For a query that needs to revisit source entities whose attributes changed, view.getAttributeOverrideEntityIds() returns a snapshot of the current named, positional and type override IDs, including edits made without history records. It is a candidate set: use effective entity reads to check deletion, the live type and attribute layout. Created entities are enumerated separately by iterateEffectiveEntityIds.

Compound undo records

recordCompoundMutation(view, draft => ...) runs a synchronous edit atomically and records the complete earlier overlay as one operation. Write through the supplied draft. This supports commands that replace profiles or axes and forget entities created earlier in the session, as well as commands that create graphs.

undoRecordedMutationOperations(view, count, revertRaw) restores the last count operations atomically. Recorded compounds restore their whole earlier graph and journal; revertRaw(draft, mutation) handles other journal entries. The result counts mutation records reverted. A thrown inverse leaves both IFC state and compound history unchanged. Both edit and inverse callbacks must be synchronous. Retained history stores only changed overlay entries and removed journal records; unchanged graph and journal prefixes are not copied per operation. Allocated express IDs remain monotonic. This in-memory history belongs to the live view and does not survive serialization.

Whole numbers on REAL-typed attributes

ISO 10303-21 requires a REAL-typed attribute (IfcLengthMeasure coordinates, profile dimensions, IfcExtrudedAreaSolid.Depth, …) to carry a decimal point — 450., never 450. The exporter is schema-aware: when a slot's declared type is unambiguously REAL-backed, a whole-number value is serialized with the decimal point automatically, so the natural call just works:

const profile = editor.addEntity('IfcRectangleProfileDef', [
  '.AREA.', null, '#34', 0.6, 0.4,
]);

editor.setPositionalAttribute(profile.expressId, 3, 1);  // XDim → 1.  (dotted)

For the rare slot that a bare value genuinely can't disambiguate — a SELECT(IfcInteger, IfcReal) where you specifically want the REAL member — wrap the number in the write-only { real } marker to force a REAL literal:

const unitRef = '#34';  // STEP reference to an IfcNamedUnit

editor.addEntity('IfcQuantityLength', ['L', null, unitRef, { real: 3 }]); // → IFCLENGTHMEASURE-safe 3.

Type-qualified values on SELECT attributes

ISO 10303-21 requires a SELECT member that is a defined type to be type-QUALIFIED — IFCBOOLEAN(.T.), not a bare .T., in an IfcTranslationalStiffnessSelect slot. The exporter auto-qualifies these when the member is unambiguous, so the natural call just works:

// IfcBoundaryNodeCondition: Name, TranslationalStiffnessX/Y/Z,
//                           RotationalStiffnessX/Y/Z
const condition = editor.addEntity('IfcBoundaryNodeCondition', [
  'Pinned', null, null, null, null, null, null,
]);

// TranslationalStiffnessX : SELECT(IfcBoolean, IfcLinearStiffnessMeasure)
editor.setPositionalAttribute(condition.expressId, 1, true);  // → IFCBOOLEAN(.T.)
editor.setPositionalAttribute(condition.expressId, 1, 1000);  // → IFCLINEARSTIFFNESSMEASURE(1000.)

When the SELECT has several members of the same primitive class (a number in IfcValue, which has 100+ REAL-backed members) auto-qualification can't choose. Use the write-only { typed: { type, value } } marker to pin the exact type — it also works for the whole IfcValue family (NominalValue, etc.) and subsumes { real }:

// IfcPropertySingleValue: Name, Description, NominalValue (IfcValue), Unit
editor.addEntity('IfcPropertySingleValue', [
  'Length', null, { typed: { type: 'IfcLengthMeasure', value: 3 } }, null,
]); // NominalValue → IFCLENGTHMEASURE(3.)

Mutation history (for undo / export)

const mutations = view.getMutations();
//   [{ id, type: 'UPDATE_PROPERTY', entityId, psetName, propName, oldValue, newValue, ... }]

console.log(view.hasChanges(wallExpressId));  // true
console.log(view.getModifiedEntityCount());   // 1

Reset back to the source data:

view.clear();

Serializing history: exportMutations / importMutations

const json = view.exportMutations();
// → ship to a teammate, persist, or replay onto another MutablePropertyView

mutationView.importMutations(json);

importMutations is not a full inverse of exportMutations for created entities. A CREATE_ENTITY record (from view.createEntity(...)) carries only the expressId in the history — not the entity's type and attributes — so importMutations cannot rebuild the entity from the record alone. It logs a console.warn and skips the record, and also drops every other mutation recorded against that same entity id in the same batch (so the round trip is lossy — the entity and its edits are both dropped — rather than leaving an orphaned property/attribute/quantity keyed to an id that was never created on the receiving view).

To carry a created entity across, call restoreNewEntity() with its NewEntity payload (read via getNewEntity/getNewEntities on the source view) before calling importMutations:

Native createEntity() records carry creationId, matching their original CREATE_ENTITY mutation UUID. This is provenance metadata, not an IFC attribute. Preserve the original token when cloning an original record for undo or recovery. A tokenless replacement body cannot borrow an old journal entry as identity, even if its express ID and fields match.

const json = view.exportMutations();

const created = view.getNewEntity(expressId)!;
mutationView.restoreNewEntity(created);
mutationView.importMutations(json); // dependent property/attribute/quantity mutations now replay

Mutations recorded against a pre-existing (source-buffer) entity always round-trip — this caveat is scoped to entities created via createEntity / StoreEditor.addEntity.

Bulk updates

Use @ifc-lite/rules to evaluate property filter groups and pass the resulting Express IDs in select.expressIds.

Spatial filters (storeys, buildings, sites, spaces) use the supplied SpatialHierarchy for an unchanged source model. After session edits, supply a live container-membership reader as the seventh BulkQueryEngine constructor argument. A spatial filter throws if its hierarchy is missing or an edited session has no live reader, so a bulk edit cannot silently target a wider set.

import { BulkQueryEngine } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';

const engine = new BulkQueryEngine(store.entities, view);

const result = engine.execute({
  select: {
    entityTypes: [/* IfcWall enum value */],
  },
  action: {
    type: 'SET_PROPERTY',
    psetName: 'Pset_WallCommon',
    propName: 'ThermalTransmittance',
    value: 0.18,
    valueType: PropertyValueType.Real,
  },
});

console.log(`Updated ${result.affectedEntityCount} walls`);

Preview without applying:

import { BulkQueryEngine, type BulkQuery } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';

const engine = new BulkQueryEngine(store.entities, view);

// The same query the execute example above runs.
const bulkQuery: BulkQuery = {
  select: {
    entityTypes: [/* IfcWall enum value */],
  },
  action: {
    type: 'SET_PROPERTY',
    psetName: 'Pset_WallCommon',
    propName: 'ThermalTransmittance',
    value: 0.18,
    valueType: PropertyValueType.Real,
  },
};

const preview = engine.preview(bulkQuery);
console.log(`Would update ${preview.matchedCount} entities`);

CSV import

Map a spreadsheet column to a pset/property in one call:

import { CsvConnector } from '@ifc-lite/mutations';
import { PropertyValueType } from '@ifc-lite/data';

const connector = new CsvConnector(store.entities, view);

const csvText = await file.text();  // or any string of CSV

const stats = connector.import(csvText, {
  matchStrategy: { type: 'globalId', 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 },
  ],
});

console.log(`Matched ${stats.matchedRows} / ${stats.totalRows} rows, applied ${stats.mutationsCreated} mutations`);

Change sets — group + share

import { ChangeSetManager } from '@ifc-lite/mutations';

const manager = new ChangeSetManager();
const changeSet = manager.createChangeSet('Fire safety pass — round 2');

manager.addMutation(mutation1);
manager.addMutation(mutation2);

const json = manager.exportChangeSet(changeSet.id);
// → ship to a teammate or persist to disk

const restored = manager.importChangeSet(json);

Pair this with exportToStep(store, { applyMutations: true }) from @ifc-lite/export to write a real .ifc file with the changes baked in.

Features

  • Mutation overlay on read-only IFC data
  • Undo/redo support (via viewer store)
  • Change sets for grouping related mutations
  • Bulk query engine for updating many entities
  • CSV import for spreadsheet-based updates
  • Store-level edits: StoreEditor for addEntity / removeEntity / setPositionalAttribute over a parsed IfcDataStore
  • Export modified data

API

See the Property Editing Guide and API Reference.

License

MPL-2.0