Skip to content

Changelog ​

Each crate in the family owns its own changelog and is versioned independently: a release of one does not imply a release of any other. This page collects them, newest version first, with the crates that changed in each. Entries follow Keep a Changelog.

History up to and including 0.2.0 -- when the family was released in lockstep -- is archived in the repository changelog.

[Unreleased] ​

ifc-model ​

Added (#465) ​

  • Inverse, Restored, InverseError: the recorded inverse of a run of model writes (Inverse::record, Model::begin_recording and end_recording). Applying one restores the model exactly (file order, type-index order, attribute counts, header, next free id) and returns the redo.
  • History and HistoryError: a bounded undo and redo stack of inverses; History::track records any change as one entry.
  • Transaction::commit_reversible, Transaction::set_header and Transaction::header.

Changed (breaking) (#465) ​

  • Model::header_mut bumps the revision. A Transaction opened before a header_mut edit is now refused at commit with Conflict::StaleRevision; set the header first, or stage it with Transaction::set_header.
  • Revisions are process-unique: every change draws a new value from one process-wide counter, so values are increasing but no longer consecutive, and two models never share a revision once either was written. A Transaction or Inverse made against one model is refused by another. A clone keeps its source's revision until either side changes.
  • Model::set_attributes with an empty edit list changes nothing and no longer bumps the revision.
  • Model::clone does not copy an open recording.

Semver: the revision changes can refuse code that committed before, so this release is 0.4.0, not a patch.

ifc-template-catalog ​

Fixed (#537) ​

  • The XML importer trims DataType@type and reftype: about 100 IFC2X3 entries (IfcBoolean ) and one IFC4X3 entry (IfcDuration ) carried trailing spaces and so named no type. The embedded container, the IFC2X3 and IFC4X3 snapshot pins and TSV exports are regenerated from the pinned sources (source digests unchanged).

Semver: patch.

ifc-validate ​

Added (#537) ​

  • property.undeclared_member and property.member_type warnings (bSI PSE001): for a Pset_/Qto_ set the release's catalog defines, a property or quantity the catalog's template does not declare, and a set kind, entity form or value type that differs from it. A value type passes when it is the declared one or a defined type aliasing it. Names match exactly; a case-only difference is mentioned in the message.
  • PropertySetCatalog::template, a provided method returning the neutral CatalogSet (CatalogSetKind, CatalogMember, CatalogMemberForm, CatalogQuantityKind). The default returns None.
  • FindingDetail::CatalogMember(CatalogMemberDetail) with CatalogMismatch.

Changed (#537) ​

  • A catalog whose template gives none for a set it defines gets one unsupported finding per new rule id ("the members of N catalog sets were not checked"); a declared value type the release's tables lack gets one unsupported property.member_type finding. Neither is silent.

  • A declared SELECT type (IfcValue, as in BendingParameters) takes any defined type its closure holds, and a single value declared with an entity type (the IFC2X3 *PHistory sets: IfcTimeSeries) is written as an IfcPropertyReferenceValue to that entity, which is checked against it.

Known false-positive source: some catalog templates disagree with the schema or with each other (Qto_BuildingElementProxyQuantities, ProcessItem sets); those are catalog data issues tracked in #553, not fixed here.

Semver: minor (0.8.0): runs with a template-giving catalog report new warnings, and PropertySetCatalog, FindingDetail and the new types grow the public API.

openbim-ifc ​

Changed (breaking) (#465) ​

  • Through the re-exported Model: header_mut bumps the revision, so a Transaction opened before a header edit is refused at commit with Conflict::StaleRevision; revisions are process-unique rather than per-model counters, so a transaction or inverse made against another model is refused (ifc-model 0.4.0).

Added (#465) ​

  • Re-exports History, HistoryError, Inverse, InverseError and Restored from ifc-model: History::track around apply_authoring (or any change) makes it undoable with byte-identical restoration.

Changed (#559) ​

  • The GlobalIds a property edit derives for the sets and relationships it creates derive from a caller seed, the object's GlobalId, the set name, the record's role and its count among the batch's records of that object, set and role, no longer from the model's next free id. The same edits under the same seed give the same GlobalIds after a write/read cycle that shifts the next id, so a journal of property edits replays to the same identities.
  • stage_property_edits and apply_property_edits take that seed, and so does every write that may create a set: SelectorIndex::stage_set_value, selector::set_value, SelectorIndex::plan_text_edits, selector::apply_text_edits, sheet::plan_import and sheet::import (an attribute write ignores it).
  • fresh_seed is available in every build, no longer only with authoring.

Added (#559) ​

  • PropertyEditFailure::SeedReused, raised when a derived GlobalId is held by an entity the batch leaves in the model (the seed named an earlier batch's records). A seed may edit other objects or sets, and may be used again once its records are removed.

Semver: breaking (minor while 0.x), released with #548 in 0.38.0: the property-edit, selector-write and sheet-import entry points take a seed, the same edit of the same file no longer gives the same GlobalIds, and SeedReused is a new variant of the #[non_exhaustive] PropertyEditFailure.

Changed (#548) ​

  • The GlobalIds a batch derives (an IfcRoot created without one, every relationship a builder writes, copies) derive from the seed, the operation's position in the batch, the created entity's type and its count among that type in the operation, no longer from the entity id; an apply_append derives from the seed, the type and the count. The same seed and operations give the same GlobalIds after a write/read cycle that shifts the next id, so a journal of batches replays to the same identities. An AssignType that joins an existing relationship derives none.

Added (#548) ​

  • AuthoringFailure::SeedReused, raised when a derived GlobalId is held by an entity of the model (the seed named an earlier batch). A seed whose entities were all removed may be used again.

Semver: breaking (minor while 0.x): a fixed seed gives other GlobalIds than 0.37.0, and a batch that reuses a seed may now be refused; SeedReused is a new variant of the #[non_exhaustive] AuthoringFailure. The hosts draw a fresh seed per batch (the browser from Math.random).

Fixed (#534) ​

  • AuthorOp::AssignType on a type that already has an IfcRelDefinesByType adds the objects to that relationship's RelatedObjects instead of creating a second one, which broke IfcTypeObject.Types SET [0:1] (ObjectTypeOf in IFC2X3). The op produces the existing relationship's id, and owner_history applies only to a relationship that is created. An object typed by another type is still refused unless replace. A relationship whose RelatedObjects is not a list is refused with InvalidRelationship, not rewritten.

Added (#537) ​

  • ReleaseCatalog gives ifc-validate the member templates of the embedded or installed PSD/QTO catalog, so validate reports property.undeclared_member and property.member_type warnings for the members of a catalog set. A set kind the build does not know gives no template, and its members are reported as not checked.

Semver: minor; it re-exports ifc-validate 0.8.0 and its new findings.

Fixed (#537) ​

  • The property writer's template check accepts any type a declared SELECT (IfcValue) closes over, as validate does; it refused them before.

Changed (#539, tests) ​

Semver: no release on its own. No code change.

  • The #469 corpus test length_unit_corpus.rs sums mesh volumes about each body's first vertex rather than the world origin, where triple products at georeferenced coordinates cancelled catastrophically. The 29 products it skipped as ill-conditioned (blamed on booleans decided by the last bit) were that measuring artefact: the skip list is removed, and all 181 meshed products are compared and agree after converting to feet.

openbim-ifc-cli ​

Changed (#559) ​

  • edit and import-table derive the GlobalIds of the sets and relationships they create from a seed drawn per run, no longer from the file's next free id. Behaviour change: the output of edit and import-table is no longer reproducible between runs; the same edit of the same file gives new GlobalIds on each run.

Semver: patch (commands, flags, exit codes and documents unchanged; only the GlobalIds of newly created records differ per run).

Added (#538, EXPRESS rules) ​

  • validate --express FILE.exp (repeatable, one file per release) evaluates every WHERE, type and global rule of each file's release from the EXPRESS text you supply. A file with no matching --express is refused with the new kind express-mismatch; a file that does not parse is invalid-express. Both exit 2.
  • The JSON summary gains indeterminate, always present; the human summary adds it when --express is given. An indeterminate rule never fails the run.

Semver: minor (additive; exit codes keep their meaning).

Changed (#537) ​

  • validate also reports the property.undeclared_member and property.member_type warnings for the members of a catalog set, when the build carries the property catalog.

Semver: no code change; patch when released.

[0.37.0] - 2026-10-20 ​

openbim-ifc ​

Added (#479, copy and append) ​

  • AuthorOp::CopyProduct { product, options } copies an IfcProduct or IfcTypeObject with what RemoveProduct would remove with it, by the #478 cascade's own reference count, and shares what it would keep. Copied, with new GlobalIds: the product; its ObjectPlacement (relative to the same parent; CopyPlacement::Offset moves it in the parent's frame, CopyPlacement::At replaces it); its representation by CopyRepresentation's rules (a type's representation maps with their origins); the property and quantity sets only it is defined by; its material usage; its ports; and, by CopyProductOptions::openings and fillings (both on), its openings and their fillings, placed relative to the copied placements. Shared, the copy joining the original's relationship: its type assignment, shared property sets, classification, document and library references, groups, layers and, by container and aggregate (both on), its spatial container and its whole. Relationships that would bind another object (connections, space boundaries, parts, a type's occurrences) are skipped and reported. Entities a UNIQUE rule binds (IfcPropertyEnumeration, IfcApplication) are always shared.
  • AuthorOp::CopyEntity { entity, policy }: a deep copy along attributes by an explicit CopyPolicy (classes always copied, always shared; other IfcRoots and definitions shared unless asked).
  • CopyReport (copied, reused, joined, skipped, regenerated, each original with its copy) in AuthoringOutcome::copies and StagedAuthoring::copies.
  • apply_append and stage_append copy entities of another model with their closure: what they reference, their property sets, type, material, classification, openings, fillings and ports, and what attaches to their geometry; relationships to objects not copied are skipped. Equal units, contexts, owner history, materials and styles are reused (ReuseOptions); GlobalIdMode keeps, merges by identity (joining the target's relationships) or regenerates GlobalIds. Attributes move by name between releases.
  • AuthoringFailure::UnsupportedCopy, IncompatibleRelease (a type, attribute or enumeration constant the target release lacks; never dropped) and UnitMismatch (differing length or plane angle units; the message points at unit conversion, #469).

Changed (#479) ​

  • The operation options moved from authoring/op.rs into their own module under the 800-line cap; every public path is unchanged.

Semver: additive (AuthorOp, AuthoringFailure, AuthoringOutcome and StagedAuthoring are #[non_exhaustive]): minor.

Added (#461, #511) ​

  • Feature validate-express: ifc::validate::express, the EXPRESS rule evaluator of ifc-validate (every WHERE rule, type rule and global rule of a release, from the .exp text the caller supplies). In full.
  • ifc::validate is a module re-exporting ifc-validate whole. With property-catalog or property-catalog-runtime, its validate, validate_with, validate_declared and validate_with_options check Pset_/Qto_ set names against the declared release's catalog (ReleaseCatalog): a set the catalog does not define is a property.reserved_set_name warning. Without a catalog the names are reported once as unsupported.

Semver: minor.

Added (#469) ​

  • convert_length_unit and stage_length_unit_conversion: rescale every length-dimensioned value of a model to a new project length unit (an SI METRE with any prefix, or inch, foot, yard, mile, US survey foot) as one checked transaction. Which values are lengths, areas and volumes comes from the declared release's tables (IFC2X3, IFC4, IFC4X3), through every attribute, aggregate and typed SELECT value: geometry, placements, profiles, quantities and property values. Values with a unit of their own keep it. Every REAL and IfcParameterValue attribute of the three releases is reviewed (a test fails on an unreviewed one): IfcGeometricRepresentationContext.Precision rescales, light attenuations rescale inversely, parameter values follow their curve or surface (kept on lines, conics, polylines, B-splines, clothoids and composite curves of those; rescaled on planes and along a cylinder's axis). Georeferencing keeps map coordinates in a MapUnit and rescales Scale; IFC2X3 ePSet_MapConversion likewise. Areas and volumes follow the length unit by default (LengthUnitOptions). Refused, each by name, with nothing written (LengthUnitError::Refused): parameter values on curves whose parameter space IFC leaves undefined (other spirals, IfcCurveSegment, IfcOffsetCurveByDistances), p-curves on curved surfaces, IfcPolynomialCurve coefficients, texture generator parameters, untyped numbers in a SELECT, contexts with their own units. The LengthUnitReport lists what was rescaled and kept. Verified over the fixture corpus: a millimetre-metre round trip restores every value, compiled geometry agrees in metres after converting to feet, and validation findings are unchanged.

Semver: minor (additions).

Added (#472, #474, values by path as text and tables) ​

  • ifc::selector::value_text writes a SelectedValue as the text of one cell (TRUE, the shortest decimal of a real, #12, a null text), and SelectorIndex::plan_text_edits plans many TextEdits (entity, path, text) as one transaction: each text read as a value of its target's declared type (attributes) or stated type (properties), IFCLABEL(F90) naming one where nothing states it; a value already held left alone; every refusal collected as an EditRefusal naming its edit. An EditPlan lists each PlannedChange (target, before, after) and commits as one transaction, refused if the model changed since. apply_text_edits plans and commits.
  • SelectorError::InvalidText and SelectorError::ConflictingWrite (the enum is #[non_exhaustive]).
  • ifc::sheet (feature sheet, implies selector): export writes entities by value paths as a Sheet keyed by GlobalId or id (KeyColumn, SheetOptions with a null text); plan_import reads a sheet back by key into an ImportPlan of CellChanges, or every SheetRefusal by row and column (header spans, missing, empty, unknown, ambiguous and repeated keys, ragged rows, per-cell write refusals); import commits it as one transaction. An unchanged sheet plans no change and commits nothing. Sheet::read and Sheet::write take CSV (RFC 4180) and TSV with escapes; XLSX with feature sheet-xlsx, through rust_xlsxwriter 0.100 and calamine 0.37 (pure Rust, on the zip 9 and quick-xml 0.42 already in the tree, no default features), every cell written as text. full includes sheet-xlsx.

Semver: minor (additive).

Changed (#512) ​

  • product_curves reads a representation that also holds text or a fill area. A dimension IfcAnnotation (its lines as IfcPolylines, its value as an IfcTextLiteralWithExtent) was refused as a whole; its lines now come back as polylines, and each IfcTextLiteral, IfcTextLiteralWithExtent and IfcAnnotationFillArea of the representation is listed in the new ProductCurves::skipped (a SkippedItem: the item and its NonCurveItem kind), not drawn and not a refusal. Read those items with ifc_style::StyleView. Any other item that cannot be lowered still refuses the product.
  • SkippedItem and NonCurveItem are re-exported from ifc-geometry.

Semver: minor. ProductCurves is #[non_exhaustive] and gains a field; products that were refused now return their curves.

Changed (#467) ​

  • validate::validate_declared refuses a file declaring IFC4X3_TC1 or IFC4X3_ADD1 as ValidateError::UnpublishedSchema, as ifc_validate::validate_declared does: a release buildingSMART publishes no EXPRESS for, never validated as IFC4X3 ADD2.

Semver: minor, following ifc-validate.

[0.36.0] - 2026-10-18 ​

openbim-ifc ​

Changed (#506, #509) ​

  • Requires ifc-geometry with #506 and #509 (axiolid-mesh-compile 0.3.28). product_curves returns certified polylines for trimmed, composite, offset and intrinsic curves; a curve the reference compiler has no certified polyline for is refused as GeometryError::CompilationRefused with its reason, naming the product. product_meshes compiles advanced B-reps whose edges cover part of their line, circle or ellipse, with either SameSense.
  • geometry::GeometryError::PolylineUnsupported and geometry::compile::UnpolylinedCurve, re-exported from ifc-geometry, are gone.

Semver: minor, following the dependency.

[0.35.0] - 2026-10-16 ​

openbim-ifc ​

Changed (#515, #519) ​

  • Requires ifc-geometry with #515: every IfcPolygonalBoundedHalfSpace lowers to Axiolid's BoundedHalfSpaceAlongZ, its prism along Position's Z (axiolid/kernel#321). Oblique clips, such as gable walls under sloped roofs, mesh and compile exactly, non-convex and arc boundaries included; product_meshes cuts small clips over tall subjects to their full depth (axiolid/kernel#325, #519). product_graphs labels a payload with such a clip wire format 1.4 (axiolid-model 0.3.14); every other payload keeps its version and bytes.

Semver: minor, following the dependency.

[0.34.0] - 2026-10-14 ​

openbim-ifc ​

Added (#458, curves and surfaces as output primitives) ​

Semver: minor (additive; ProductMesh is #[non_exhaustive]).

  • product_curves(model, products, purpose) (feature mesh): each product's curves for a RepresentationPurpose as ProductCurves (world placement, one ProductPolyline per curve: the IFC curve, f32 positions relative to the placement, closed, chord_bound and chord_requested). Plan takes the drawing curves (Plan, then Annotation, FootPrint, Axis), and Axis, FootPrint, Annotation one identifier. A trimmed, composite, offset or intrinsic curve refuses the product with GeometryError::PolylineUnsupported until the reference compiler polylines it (#506).
  • product_meshes_for(model, products, purpose): the surfaces and solids of another representation, such as Surface, as one mesh; its curves are left to product_curves. product_meshes is it with Body.
  • ProductMesh::closure: Solid, Surface (an area, no volume) or Unknown for an empty mesh. Only a Body mesh resolves a colour; a mesh of another representation carries Ok(None).
  • RepresentationPurpose and select_product_representation are re-exported at the root.

Changed (#458) ​

  • A Body whose items mix curves with solids or surfaces now meshes those and leaves the curves to product_curves; before, the reference backend refused the whole Body. A Body with no curves meshes exactly as before.

[0.33.0] - 2026-10-14 ​

openbim-ifc ​

Changed ​

  • Requires ifc-author 0.3.2, whose builder accepts nested aggregates that the #481 representation ops write.

Added (#462, INVERSE attributes by name) ​

  • attribute_value_by_name(&model, schema, &reverse, id, name) reads an explicit attribute (AttributeValue::Explicit) or an INVERSE one (AttributeValue::Inverse): every referrer whose type is the FOR entity or a subtype in the declared release and whose FOR attribute (the slot that release gives it) holds the entity, directly or as an aggregate member, read from a ReverseIndex the caller builds once. InverseValue::Aggregate(ids) for a SET or BAG, SET [0:1] included (zero or one id, never unwrapped), in ascending id order; InverseValue::Single(Option<EntityId>) for a non-aggregate inverse (VoidsElements, IFC2X3's IfcGroup.IsGroupedBy), refused with NamedAttributeError::InverseCardinality naming every referrer when more than one references it. Both enums are #[non_exhaustive].
  • inverse_slots(schema, type_name) and inverse_slot(..): every INVERSE attribute of a type, inherited first, as InverseSlot (name, FOR entity and attribute, aggregation with lower()/upper(), declaring entity); a subtype's redeclaration replaces the inherited one. The bundled tables' answers equal the normative EXPRESS's for every entity of IFC2X3, IFC4 and IFC4X3 (a spec-backed test).
  • total_inverses(&reverse, id): the distinct entities referencing one (IfcOpenShell's get_total_inverses). ReverseIndex and Referrer are re-exported at the root; ReverseIndex::referrers is the get_inverse(.., with_attribute_indices=True) equivalent.

Changed (#462) ​

  • attribute_slot, attribute_by_name, set_attribute_by_name and attribute_type refuse an INVERSE name with the new NamedAttributeError::InverseAttribute instead of UnknownAttribute; UnknownAttribute now reads "has no attribute".

Semver: additive (NamedAttributeError is #[non_exhaustive]), a minor release while 0.x.

Added (#481, shape representations) ​

  • AuthorOp::ShapeRepresentation { context, items, identifier, representation_type } assembles an IfcShapeRepresentation of existing items in a geometric representation context. A given RepresentationType is checked against the items by the declared release's IfcShapeRepresentationTypes (CorrectItemsForType, IFC2X3 WR24), transcribed per release family (IFC2X3; IFC4, IFC4X1, IFC4X2; IFC4X3), with IfcCurve.Dim derived as IfcCurveDim does; without one, the most specific listed type admitting every item is inferred. A label the release does not list is written as given (its rule returns ?). The identifier defaults to the context's ContextIdentifier and is required from IFC4 on (HasRepresentationIdentifier); CorrectContext and NoTopologicalItem are checked.
  • AuthorOp::AssignRepresentation { representation, to }: to an IfcProduct, appended to its IfcProductDefinitionShape (created when missing; produces it); to an IfcTypeProduct, wrapped in a new IfcRepresentationMap at the origin and appended to RepresentationMaps (produces the map). A representation already in a product shape, representation map or shape aspect is refused (IfcShapeModel.WR11).
  • AuthorOp::UnassignRepresentation { representation, from }: off the product shape (an emptied one goes, with its shape aspects, and the product's Representation is unset) or the type (its map goes with its origin); the representation stays, unassigned. Reports what went.
  • AuthorOp::CopyRepresentation { representation }: a copy of every representation item and profile definition it reaches, and of the styled items and colour or texture maps attached to them; the context, styles, presentation items, the type maps mapped items map and every other resource are shared. The copy joins the original's layers.
  • AuthorOp::RemoveRepresentation { representation }: taken off its uses, then removed by the #478 cascade, shared items, profiles, points and styles kept and reported. AuthoringOutcome::reports and StagedAuthoring::reports carry the report of an unassignment or a representation removal.
  • AuthoringFailure::RepresentationTypeMismatch, NoRepresentationType, UndecidableRepresentationType, MissingRepresentationIdentifier, InvalidRepresentationItem, SharedProductShape and NotAssigned.

Semver: additive (AuthorOp and AuthoringFailure are #[non_exhaustive]): minor.

Added (#464, selector query language) ​

  • ifc::selector (feature selector; in full): a selector grammar, version 1.0 (GRAMMAR_VERSION), close to IfcOpenShell's util.selector: class facets (subtypes included) and GlobalId facets with ! negation, explicit attribute facets, property and quantity facets (Pset_WallCommon.FireRating=F90, names or /patterns/), type, material, classification, location (alias container) and group keywords, query: value paths, comparisons =, !=, *=, !*=, >, >=, <, <= against text, numbers, NULL, TRUE, FALSE and patterns, , facet lists, + unions and /* */ comments. Selector::parse and ValuePath::parse give a typed AST that prints back in canonical form; every ParseError carries the byte span it refuses.
  • SelectorIndex evaluates a selector (select, select_from) over the model's type index, a GlobalIdIndex, the exact resolver's PropertyIndex (feature properties; FeatureDisabled without) and one scan per relationship kind, each built on first use. A class or attribute the release does not declare is refused with its span before any entity is read; contradictory or dangling relationships are refused, never skipped. Results are in ascending id order.
  • Values by path (SelectorIndex::value, get_value): attributes, property sets and properties, type, material(s), container, parent, space, storey, building, site, occurrences, classification, group, class, id, predefined_type, count and list indices, as SelectedValues.
  • Writes by path (set_value, SelectorIndex::stage_set_value, set_targets, stage_attributes, property_edits): an attribute of every entity the path reaches, checked against its declared type in the release, or a property through the checked property edit, staged on one transaction; SelectorError names every refusal.
  • Depends on regex-lite (optional, with selector).

Semver: minor (additive).

Added (#480, type propagation on assignment) ​

  • AuthorOp::AssignType takes options: AssignTypeOptions. With map_representations, each of the type's RepresentationMaps is mapped onto every object as an IfcShapeRepresentation (the map's context and RepresentationIdentifier, RepresentationType MappedRepresentation) holding one IfcMappedItem, whose MappingTarget is mapping_target or a fresh identity IfcCartesianTransformationOperator3D (2D for a 2D MappingOrigin); the representations join the object's IfcProductDefinitionShape, created when it has none. With material_usage, an IfcMaterialLayerSetUsage (LayerSetDirection from layer_set_direction or the class: AXIS2 for IfcWall, AXIS3 for IfcSlab and IfcPlate, as the IFC4 ADD2 TC1 documentation fixes them; POSITIVE; offset 0) or an IfcMaterialProfileSetUsage (no CardinalPoint) is derived from the type's set, one per object in its own IfcRelAssociatesMaterial; any other material applies through the type. predefined_type: PredefinedTypeMode is Ignore (default), Validate (refuse an object whose own PredefinedType is set, not NOTDEFINED and differs from the one the type provides; USERDEFINED compares ObjectType with the type's ElementType) or Sync (unset the object's, and its USERDEFINED ObjectType, where the type provides one). replace unassigns each object's previous type: it leaves its IfcRelDefinesByType (removed when emptied), and the representations mapped from the previous type's maps and the usage derived from its set are removed with what only they use, by the #478 cascade's ownership rules. AssignTypeOptions::propagate() sets the three propagations.
  • AuthorOp::Contain and AuthorOp::Aggregate take options: RelateOptions with replace, which moves an object out of its previous container or whole instead of refusing it.
  • AssignTypeOptions, RelateOptions, PredefinedTypeMode, LayerAxis.
  • AuthoringFailure::RepresentationConflict, UnsupportedRepresentationMap, MaterialUsageUndetermined and PredefinedTypeConflict; a material on an object that already has one is AlreadyRelated with IFCRELASSOCIATESMATERIAL.

Refusing a second type, container or parent stays the default, and every option is off by default: a batch that sets none writes what it wrote before. Semver: minor while 0.x. The three operations gain a field, so a struct literal of them needs options (AssignTypeOptions::new(), RelateOptions::new()); new variants of #[non_exhaustive] enums and new #[non_exhaustive] types are additive.

[0.32.0] - 2026-10-13 ​

openbim-ifc ​

Added (#478, cascading delete and purge) ​

  • AuthorOp::RemoveProduct { product, options } removes an IfcProduct or IfcTypeObject with everything it solely owns: its ObjectPlacement, its representation down to the items nothing else uses, the property and quantity sets only it is defined by with their properties, its material usage (not the material definitions), its openings (each with the same cascade) and its ports (nested, or connected in IFC2X3), and every relationship left without an end. "Solely owned" is a reference count over the batch's view (the base model's ReverseIndex, corrected for the batch's own changes), taken to a fixed point; a relationship that only loses a list member and a presentation layer assignment do not count, a styled item and a shape aspect go with what they attach to. Shared data is kept: a property set another object uses, a type's representation maps, a style or point used elsewhere; so are related objects (the type, the container, the fillings of a removed opening) and definitions (material definitions, classification references, units, contexts, the owner history).
  • RemoveProductOptions (#[non_exhaustive]): rebase_placements re-bases placements relative to a removed placement onto its nearest kept ancestor with the composed IfcAxis2Placement3D transform, so nothing moves; untype_occurrences lets a type object with occurrences go, removing its IfcRelDefinesByType.
  • AuthorOp::Purge(PurgeOptions): without classes, every resource entity no IfcRoot reaches, nor annotates something reached (styled items, layer assignments, shape aspects, resource relationships, coordinate operations); with classes, the unreferenced entities of those classes. IfcRoots (include_roots; IfcProject only when named), the owner history with its actors (include_owner_history) and, by default, representation contexts are kept unless asked.
  • RemovalReport (removed, kept with a KeepReason and the users of a shared entity, detached, rebased), RemovedEntity, KeptEntity, KeepReason (Shared, RelatedObject, Definition, Protected): AuthoringOutcome::reports and StagedAuthoring::reports carry one per operation, so staging a batch is the dry run.
  • AuthoringFailure::PlacementHasDependents, TypeHasOccurrences and UnsupportedRebase.

Changed (#478) ​

  • The authoring batch reads who references an entity from the base model's ReverseIndex, built once per batch on the first removal, instead of scanning the model per lookup.

Semver: additive (new variants of #[non_exhaustive] enums, new fields of #[non_exhaustive] structs), a minor release while 0.x.

Added (#450) ​

  • ifcZIP, behind the new zip feature (on in full): ifc::ifczip reads an archive holding one STEP or ifcXML model at its root (buildingSMART implementation agreement CV-2x3-154), the inner format and the ifcXML layout detected from the content, through the existing codecs (read, extract, read_entry), and writes one Deflate entry (write, pack), reproducibly. Refusals are typed (IfcZipError): Malformed, NoModel, SeveralModels, UnknownInnerFormat, Encrypted, UnsupportedCompression (anything but Stored and Deflate), BudgetExceeded and CodecNotCompiled. Decompression is bounded by a Budget, absolute (4 GiB) and as a ratio to the compressed size (200, above 1 MiB), checked against the declared size and again while inflating.
  • ifczip::IfcZipCodec puts ifcZIP behind the Codec interface: codecs() lists it and read_path reads .ifczip (an archive is recognized by its ZIP signature; without the feature it is refused as WrongFormat naming the feature).
  • xml_layout::declared_xml_layout and DeclaredXmlLayout (feature ifcxml): the ifcXML layout a document's root declares, and whether its attributes are named by position, for readers handed a document without its layout.
  • Dependencies: zip 9 with every default off and Deflate alone, through flate2's pure-Rust miniz_oxide backend; only with zip.

Semver: minor (additive; new feature and items).

Added (#449) ​

  • Feature colour (geometry-select, style, material): product_colours(model, products) and ColourResolver (product_colour, item_colours) resolve the surface colour of each product's Body. Each item takes its own IfcStyledItem, then its styled presentation layer (a boolean result its FirstOperand's, a mapped item its enclosing IfcMappedItem's), then the IfcMaterialDefinitionRepresentation of the material the product, or its type, is made of. An IfcMaterial, a layer or profile and its material, layer and profile sets and their usages naming one material, IFC2X3's IfcMaterialList of one material, and constituent sets by item (the constituent whose IfcShapeAspect names the item) resolve. A set of several materials is ColourError::AmbiguousMaterial, never its first member; Body items that differ are ColourError::ItemsDiffer. The colour carries its source (ColourSource: styled item, layer, material).
  • ProductMesh::colour (mesh now implies colour): each mesh's colour, or its typed ColourError, which never refuses the mesh. Positions and indices are unchanged.

Semver: additive (ProductMesh is #[non_exhaustive]), a minor release while 0.x.

Added (#463, GlobalIds, types and openings) ​

  • GlobalIdIndex::build(&model, schema): every IfcRoot by GlobalId, read once (O(n), only IfcRoots decoded; the slot looked up by name in the release's table). get(&Guid) is Ok(Some(id)), Ok(None), or Err(DuplicateGlobalId { global_id, entities }) naming every holder in file order: a duplicated GlobalId is never answered with one of them. duplicates() lists them all and invalid() the IfcRoots whose GlobalId is unset or malformed. by_guid(&model, schema, &guid) answers one lookup by a single scan. Behind schema-api.
  • type_of(model, schema, occurrence), occurrences_of(.., type_object), openings_of(.., element), fillings_of(.., opening) and voided_element_of(.., opening), read forward from IfcRelDefinesByType (in IFC2X3 too, whose IsDefinedBy mixes it with property sets), IfcRelVoidsElement and IfcRelFillsElement, slots by attribute name. Each refuses with ObjectRelationError (#[non_exhaustive]): MissingEntity, WrongEntityType, Malformed, MissingReference, and Ambiguous for two types of one object or two hosts of one opening, where the schema allows one. Behind schema-api.
  • new_guid(): a random RFC 9562 version-4 GlobalId from the operating system (getrandom 0.3, already linked natively through ahash), or GuidError (#[non_exhaustive]). On wasm32-unknown-unknown the new js-random feature links getrandom's JavaScript backend (crypto.getRandomValues, no RUSTFLAGS needed); without it there the call refuses with GuidError::NoRandomSource. name_based_guid(parts), the version-8 name-based GlobalId the property edit and authoring already derived theirs with, is public; its output is unchanged and pinned by a test. Guid is re-exported at the root.

Semver: additive, a minor release while 0.x.

Changed ​

  • georeferencing reads IFC2X3 (#451): the ePSet_MapConversion and ePSet_ProjectedCRS property sets of the project (or else of each site) resolve with the project length unit to the same ProjectToMap as IFC4's entities. An IFC2X3 file used to be refused with UnsupportedSchema; without the sets it now has an empty list, and a malformed or incomplete set is refused with the new GeorefError variants. Property sets of those names in an IFC4 or IFC4X3 file are not read. Requires ifc-georef with #451.

Semver: minor. A release that was refused now resolves.

[0.31.0] - 2026-10-13 ​

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.25: stations on IfcOffsetCurveByDistances bases that lower to OffsetsAtStations lower with seam windows, and an offset of an offset is refused by its own name (#443, part). Semver: minor, following the dependency.

[0.30.1] - 2026-10-20 ​

ifc-geometry ​

Changed (#525, #528: axiolid-mesh-compile 0.3.31) ​

Semver: patch. No public item changes; an optional compile-feature floor and a dev floor rise.

  • Requires, behind compile-reference-backend, axiolid-mesh-compile 0.3.31 and axiolid-construct 0.3.20, and, for the tests, axiolid-measure 0.3.11, whose exact_properties now sums about a local point rather than the world origin (axiolid/kernel#343).
  • #525: advanced B-reps whose IfcEdgeCurve covers part of an IfcBSplineCurveWithKnots or IfcPolyline compile: the kernel trims a B-spline edge curve at its vertices by certified projection and a polyline exactly (axiolid/kernel#333), where 0.3.29 refused both by name. A vertex off its curve is refused by name ("not on its curve").
  • #528: an advanced B-rep whose planar and curved faces share a straight edge, such as a D-shaped half cylinder, closes: every face takes the same samples of a shared edge (axiolid/kernel#336). It compiled open.
  • A faceted B-rep face without a surface whose corners are not exactly coplanar now reports its slab width as its bound, not 0; 0 means planar. No test of ours pinned that 0.
  • Tests: the split-rim cylinder's seam as a quadratic B-spline, a linear B-spline and polylines that run past both vertices (straight, with a collinear corner between them, and turning at the vertices with tails off the cylinder), along and against their curve, IFC4 and IFC4X3: watertight at pi r^2 h, within the cylinder's extent, certified, the wall's measured deviation within the bound and the request. Seams trimmed to one straight span meet the 1 mm request (bound 9.0e-4 m); the quadratic B-spline and the polyline with a corner between the vertices are bounded piecewise and certified at 1.59e-3 m, above the request, pinned as the kernel's answer. B-spline and polyline seams a centimetre off their vertices are refused by name, replacing the TODO(#525) refusal pin. A D-shaped half cylinder (IFC4, IFC4X3) is watertight at pi r^2 h / 2, certified within the request (bound 4.5e-4 m). All three tests fail against 0.3.29.

[0.30.0] - 2026-10-20 ​

ifc-geometry ​

Changed (axiolid-mesh-compile 0.3.29) ​

Semver: patch. No code change.

  • Requires, behind compile-reference-backend, axiolid-mesh-compile 0.3.29, a kernel packaging fix: it brings axiolid-reference 0.3.9, which requires the axiolid-evaluate ^0.3.17 it is built with (0.3.8 claimed ^0.3.12, so a resolver could pair it with an older axiolid-evaluate).

Added (#512) ​

Semver: minor. Every item below is new; compile_product_polylines and compile_product_mesh_for are unchanged.

  • compile::compile_product_curves / compile_product_curves_with: the curves of a product's representation as compile::ProductPolylines, polylines and skipped. Each IfcTextLiteral, IfcTextLiteralWithExtent and IfcAnnotationFillArea among the representation's own Items is not lowered but listed as a compile::SkippedItem (the item and its compile::NonCurveItem kind, TextLiteral or AnnotationFillArea), so a dimension (its lines as IfcPolylines, its value as text) gives its lines instead of being refused. Nothing is drawn in a skipped item's place: a fill area's boundaries are not returned. Every other item lowers as before and its refusal still refuses the product; a text literal nested in a mapped representation is lowered, so refused, not skipped. compile_product_polylines keeps the strict rule and refuses such a representation as a whole.

openbim-ifc ​

Changed (#433) ​

  • Requires ifc-geometry with #433: an IfcOffsetCurveByDistances along a bounded basis whose length is a quadrature lowers, continued to the basis's ends the kernel measures, and product_graphs labels such a payload wire format 1.3 (axiolid-model 0.3.12, axiolid/kernel#303). Every other payload keeps its version and bytes.

Semver: minor, following the dependency.

[0.29.0] - 2026-10-18 ​

ifc-geometry ​

Changed (#506, #509: axiolid-mesh-compile 0.3.28) ​

Semver: minor. GeometryError::PolylineUnsupported and compile::UnpolylinedCurve are removed.

  • Requires, behind compile-reference-backend, axiolid-mesh-compile 0.3.28 (which brings axiolid-brep-boolean 0.1.11), and, for the tests, axiolid-evaluate 0.3.17.
  • #506: compile_product_polylines returns certified polylines for trimmed, composite, offset and intrinsic curves, and for open profiles over them (axiolid/kernel#317): composite joints are shared vertices, intrinsic-curve vertices certified positions. The reference compiler still refuses by name implicit and banked curves, 3D intrinsic curves with torsion, trims of relations or of placed curves, offsets of offsets, of placed curves or of chains, offsets across a corner of their basis, 3D offsets without a reference direction and offsets by stations. Those refusals, like every other, are GeometryError::CompilationRefused carrying the kernel's reason (for example "an offset of an offset curve"), naming the product. PolylineUnsupported named the top-level curve's family whatever the kernel refused, so it would now misname a refusal inside a composite or invalid data (a composite whose segments do not meet) as unsupported; it is removed with UnpolylinedCurve, and the TODO(#506) with them.
  • #509: IfcEdgeCurve lowering stores an edge whose SameSense is .F. from EdgeEnd to EdgeStart, used reversed. The kernel now trims a line, circle or ellipse edge curve without a p-curve at its vertices, taking the edge from Edge::start to Edge::end in the curve's own sense (axiolid/kernel#319); keeping the file's vertex order with only a reversed use selected the other arc of a closed curve. An IfcSubedge of such an edge is stored the same way round. Advanced B-reps whose edges cover part of their curve compile: an IfcLine seam with a unit vector, and half-circle rim edges along and against their circles. B-spline and polyline edge curves that do not run vertex to vertex are still refused by the kernel by name (#525, axiolid/kernel#333).
  • The #443 ring deck's frame law along an ellipse's offset is now certified (axiolid/kernel#316); its deviation stays unbounded at the mitre its widening law makes at station 2 (MITRED_SECTIONS, axiolid/kernel#335). Its mesh and volume are unchanged.
  • Tests: the unit-vector seam cylinder (IFC4, IFC4X3) compiles, certified; a cylinder with split rims compiles to pi r^2 h with its half-circle edges along and against their circles in four combinations (IFC4, IFC4X3; mutation-checked against the old lowering); B-spline and polyline seams past their vertex are refused by name (TODO(#525)); the lowering of a false SameSense on an edge and a subedge; a trimmed circle axis in both senses, an offset of it, and a closed composite footprint with a trimmed arc (IFC2X3, IFC4, IFC4X3), and an IFC4X3 alignment footprint and spiral gallery, all against closed forms; an offset of an offset refused by name.

openbim-ifc ​

Changed (#429) ​

  • Requires ifc-geometry with #429: sectioned solids and surfaces along offsets whose length is a quadrature lower with seam-snapping windows on their sections, and product_graphs labels those payloads wire format 1.2 (axiolid-model 0.3.11, axiolid/kernel#301). Every other payload keeps its version and bytes: 1.0, or 1.1 when a station carries a window.

Changed (#427) ​

  • Requires ifc-geometry with #427: with an evaluator, a linear placement on an offset whose length is a quadrature (a kerb beside an alignment, an offset beside a clothoid) derives, reading a joint within the model's precision from the incoming piece.

Semver: minor, following the dependency.

[0.28.1] - 2026-10-12 ​

openbim-ifc ​

Fixed (axiolid/kernel#297) ​

  • Requires axiolid-model 0.3.10 (through ifc-geometry's wire feature): product_graphs payloads carry the lowest wire format version their content needs, 1.0 unless a station carries a seam-snapping window, then 1.1. Window-free payloads are labelled 1.0 again, readable by axiolid-model 0.3.8.

Semver: patch.

[0.28.0] - 2026-10-16 ​

ifc-geometry ​

Changed (#515, #519: polygonal bounded half-spaces along Position's Z) ​

Semver: minor. axiolid-model 0.3.14 is a normal dependency whose types (GeometryGraph, SolidOperation) are exposed, and every lowered IfcPolygonalBoundedHalfSpace changes node; the three IFCPOLYGONALBOUNDEDHALFSPACE rows leave dispatch::PARTIAL.

  • Requires axiolid-model 0.3.14 (axiolid/kernel#321, #322) and, behind compile-reference-backend, axiolid-mesh-compile 0.3.27 and axiolid-construct 0.3.19 (axiolid/kernel#321, #322, #325), which brings axiolid-evaluate 0.3.16: offsets beside a 3D intrinsic curve without torsion in a level plane are now certified (axiolid/kernel#302), and compile::window's repeated UNCERTIFIED_LENGTH text follows it.
  • Every IfcPolygonalBoundedHalfSpace lowers to SolidOperation::BoundedHalfSpaceAlongZ: the half space within the boundary's prism along Position's Z, infinite both ways, Position carried whole (its scale, shear and handedness under a mapped item). That is the IFC set for every frame, so one path replaces three: aligned clips no longer lower to BoundedHalfSpace, a normal against Position's Z is no longer re-read from its other side, and the 0.27.2 chain of unbounded half spaces for oblique or rescaled prisms (lower_prism_clip, prism_walls, the boolean hook) and its frame classification are deleted.
  • Now compiled, where 0.27.2 refused them with GeometryError::Unsupported: a non-convex boundary or one with a circular arc under an oblique or rescaled prism. Lowering refuses no frame any more; what the kernel cannot compile it refuses by name (GeometryError::CompilationRefused): a Position Z lying in the plane, and the tool as a union's operand, a first operand or a root.
  • The #515 gable wall (synthetic-compile/gable_wall_polygonal_clip_*.ifc) compiles exactly, at 49.999912042537 m³, and its mesh is certified against that body (bound about 4e-12 m); in 0.27.2 the mesh had no exact result to be measured against.
  • #519: both compilers size the cut from the subject (axiolid/kernel#325), so synthetic-compile/tall_wall_small_bounded_clip_ifc2x3.ifc meshes to the analytic 56.887388852960 m³, certified; the tests accept nothing else, and their TODO(#519) is resolved.
  • Wire format: a lowered graph holding a polygonal bounded clip is Axiolid wire format 1.4 (it was 1.0); every other graph keeps its version.
  • Tests: the gable (IFC2X3 and IFC4, gross, net and exact, certified), an L-shaped and an arc boundary under an oblique prism (analytic volumes, exact and certified), uniformly and unevenly scaled and mirrored mapped clips, and the named refusals of a Z in the plane and of a union.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.22: stations on offsets whose length is a quadrature lower with Axiolid's seam-snapping window (#423), and product_graphs encodes with axiolid-model 0.3.9, wire format 1.1 (ifc::geometry::wire::FORMAT_VERSION). An envelope carries the lowest wire version its content needs: 1.0, or 1.1 when a station carries a seam-snapping window; axiolid-model 0.3.9 labels every payload 1.1 (axiolid/kernel#297), and a reader on axiolid-model 0.3.8 or older refuses 1.1.

Semver: minor, following the dependency.

[0.27.2] - 2026-10-16 ​

ifc-geometry ​

Fixed (#515, oblique polygonal bounded half-space clips) ​

Semver: patch. No public item changes; two IFCPOLYGONALBOUNDEDHALFSPACE rows of dispatch::PARTIAL are rewritten (an oblique or rescaled prism is now admitted for a convex boundary). Only graphs that were refused before change.

  • A prism oblique to its plane, or under a scaled or sheared Position, now lowers when the half space is the second operand of a difference or an intersection and its boundary is a convex polygon. The prism of a convex polygon is the intersection of one half space per edge (the plane through the edge along Position's Z), so the subject's intersection with the tool is a chain of intersections with unbounded half spaces, and a difference is the subject minus that chain. These are identities of sets; nothing is computed in the bridge (ADR 0004). The #515 gable wall (synthetic-compile/gable_wall_polygonal_clip_ifc2x3.ifc and _ifc4.ifc) compiles to 49.999912042537 m³, IfcOpenShell 0.8.5's volume, gross and net.
  • Limits, from axiolid-mesh-compile 0.3.26: the exact compiler takes no boolean as a difference's tool, so such a mesh's deviation report is unbounded and names the missing exact result; and a prism that misses its subject is refused by the mesh compiler (CompilationRefused), not kept as the subject.
  • Still GeometryError::Unsupported naming the half space: a non-convex boundary, a boundary with an arc, a union with the tool, and the tool anywhere but a boolean's second operand. A kernel bounded half-space whose prism runs along its own placement (axiolid/kernel#321) is what covers these.

[0.27.1] - 2026-10-15 ​

ifc-geometry ​

Fixed (#515, polygonal bounded half-space clips that compiled to another tool) ​

Semver: patch. No public item changes; dispatch::PARTIAL gains three IFCPOLYGONALBOUNDEDHALFSPACE rows. Two lowered graphs change, both of which compiled to a wrong mesh before, with no error.

  • IFC bounds IfcPolygonalBoundedHalfSpace by the boundary's prism along Position's Z axis. Axiolid's BoundedHalfSpace sweeps the boundary along the clip plane's normal and frames it by Position's X projected into the plane and normal x X. Lowering now checks that the two agree before emitting the node (lower::halfspace, prism_orientation).
  • A base plane whose normal points against Position's Z (the usual "remove what is above" clip) mirrored the footprint across Position's X axis. It now lowers as the same half space read from the other side, normal reversed and side flipped, so the footprint is the authored one.
  • A prism oblique to its plane (a sloped roof plane over a vertical prism: the usual IFC2X3 gable-wall clip) has no neutral equivalent. The boundary was laid into the slope and mirrored off the wall, so compile_product_mesh and compile_product_mesh_net returned the uncut body. It is now GeometryError::Unsupported naming the half space, until the kernel has a bounded half-space whose prism runs along its own placement. A scaled or sheared Position frame, whose boundary the kernel would not scale, is refused the same way.
  • New fixtures synthetic-compile/gable_wall_polygonal_clip_ifc2x3.ifc and _ifc4.ifc (the #515 wall), and tests/bounded_halfspace_prism_axis.rs, which accepts the gable's analytic volume or a refusal naming a clip, and never the uncut body.

[0.27.0] - 2026-10-14 ​

ifc-geometry ​

Added (#458, curves and surfaces as output primitives) ​

Semver: minor. Every item below is new; RepresentationPurpose and GeometryError are #[non_exhaustive], so their new variants are additive. One refusal text changes (below); no lowered graph changes.

  • RepresentationPurpose::{Axis, FootPrint, Annotation, Surface}: each selects the first representation the product authors with that RepresentationIdentifier, in authored order, whatever its context. Body and Plan keep their policies. RepresentationPurpose::identifier, name and from_name map a purpose to and from the IFC spelling.
  • compile::compile_product_polylines(_with): the curves of a product's representation for a purpose as CompiledPolylines (the IFC curve, world points, closed, and the backend's ChordDeviation: the chord budget asked for and a certified bound on the distance from the exact curve to the polyline). It takes any PolylineCompiler, the reference backend's new operation (axiolid/kernel#311). A collection is opened, and a mapped item holding a curve places each curve on its own by the same composed transform (an exact regrouping; nothing is evaluated).
  • compile::compile_product_mesh_for(_with): the surfaces and solids of a product's representation for a purpose as one CompiledMesh with its MeshClosure. A representation with no curve compiles from its own root, so a Body's mesh is the one compile_product_mesh_reported gives. Rectangular trimmed surfaces and B-reps of loose faces now compile as MeshClosure::Surface (axiolid/kernel#311).
  • GeometryError::PolylineUnsupported { product, curve, family, reason } with compile::UnpolylinedCurve::{Trimmed, Composite, Offset, Intrinsic}: a trimmed, composite, offset or intrinsic curve the backend refuses to polyline is refused by that name, is_unsupported(), never approximated here. The reference compiler refuses them until axiolid/kernel#317 (#506).
  • compile re-exports ChordDeviation, MeshClosure, Point3 and PolylineCompiler, so a caller names no axiolid-* crate.
  • Tests: an IfcPolyline axis, a placed IfcCircle footprint far from the origin, a mapped axis, an annotation curve set, a rectangular trimmed surface, a lone IfcFaceSurface, a representation mixing both, and the trimmed and composite refusals, each in IFC2X3, IFC4 and IFC4X3.

Changed (#458) ​

  • Uses the PolylineCompiler of axiolid-mesh-compile 0.3.26 and axiolid-mesh-compile-contract 0.3.4, the floors #454 raised.
  • GeometryError::CompilationRefused reads "could not be compiled: ..." instead of "could not be compiled to a mesh: ...": it is now also the polyline compiler's refusal.

Changed (#454, curved advanced B-rep faces bounded by 3D edge curves compile) ​

Semver: patch. No public item, refusal kind or lowered graph changes. Only the dependency floors of the opt-in compile feature rise, as in 0.26.1, and bodies the reference kernel used to refuse now compile.

  • Requires axiolid-mesh-compile 0.3.26 and axiolid-mesh-compile-contract 0.3.4.
  • An IfcAdvancedBrep whose curved faces are bounded only by 3D edge curves now compiles through the reference kernel. That is how IFC writes them: IfcAdvancedFace admits no IfcPcurve as edge geometry. Before, the kernel refused such a face. Now it inverts each edge's shared 3D samples onto the face's surface (axiolid/kernel#310). The lowering is unchanged and still projects nothing (ADR 0004). A seamed cylinder with two circular caps compiles as a watertight solid. So does a B-spline face bounded by B-spline edges over planar walls.
  • A curved face's certified deviation may come out slightly above the requested chord budget. The kernel adds the face's triangle bound to its boundary terms, so check DeviationReport::meets_requested. The B-spline fixture reports 1.075 mm at a 1 mm request.
  • The kernel refuses some 3D-bounded faces, and each refusal reaches the caller as GeometryError::CompilationRefused with the kernel's reason:
    • an edge through a sphere pole (or a cone apex);
    • an edge curve whose whole domain does not run from its start vertex to its end vertex. This includes an IfcLine with a unit IfcVector, or a part of an IfcCircle: without a p-curve the kernel reads the curve's whole domain as the edge;
    • an edge off its B-spline surface. The kernel's certified inversion gives up on it as BudgetExceeded ("certified surface projection").
  • The same kernel release bounds instanced bodies with the rounding of their placement. A chord budget at or below that rounding, about 1e-9 at 6e6, is refused (axiolid/kernel#295).

openbim-ifc ​

Added (#367, geometry Level 2, ADR 0021) ​

  • Feature geometry-wire (geometry plus ifc-geometry/wire, in full): product_graphs(model, ids) -> (EntityId, GeometryResult<ProductGraph>) per product, ProductGraph { world, graph }: each product's Body lowered into Axiolid's neutral GeometryGraph exactly as product_meshes lowers it, in world coordinates (metres), with its world placement alongside and None for a product with no Body. Encode a graph in Axiolid's wire format 1.0 with GeometryGraph::to_json / to_cbor (ifc::geometry::wire). compiled_features() reports geometry-wire.

Semver: additive (a new opt-in feature), a minor release while 0.x, since the ifc-geometry floor rises.

[0.26.1] - 2026-10-14 ​

ifc-geometry ​

Changed (#443, stations along offset runs compile through the reference kernel) ​

Semver: patch. No public item, refusal kind, refusal text or lowered graph changes. Only the dependency floors of the opt-in compile feature rise, as in 0.25.1, and stations the reference kernel used to refuse now resolve.

  • Requires axiolid-mesh-compile 0.3.25 and axiolid-mesh-boolean-boolmesh 0.3.6 (and axiolid-evaluate 0.3.15 for the tests).
  • A windowed station along an IfcOffsetCurveByDistances that lowers to CurveRelation::OffsetsAtStations beside an ellipse or a B-spline now resolves through the reference kernel (axiolid/kernel#307). The kernel now certifies an offset law that varies along such a basis. 0.25.0 lowered these stations (#444), but the kernel refused them.
    • axioval/engine's deck (an IfcSectionedSolidHorizontal from 0 to 5 m along such a run beside an ellipse) now compiles with its 1e-5 m windows. Its mesh is bit for bit the one it compiles to with the windows set to zero: a closed solid of 9.99945 m3, against 2 m2 x 5 m = 10 m3 by Pappus. The kernel's deviation report for it stays unbounded: the frame law along an offset beside an ellipse has no certified derivative bounds yet (axiolid/kernel#316). This crate does not read that report.
    • Derived IfcLinearPlacements 10 m and 30 m along such a run now derive and equal the kernel's resolved stations. Before, they were refused as PlacementSeamWindowRefused with UncertifiedLength or CertificationBudgetExceeded.
  • SeamWindowRefusal::UncertifiedLength and CertificationBudgetExceeded stay. Their docs no longer name the cases the kernel now certifies. A station just past a coarsely bounded B-spline piece now pins the CertificationBudgetExceeded resource name.
  • The kernel text this crate repeats to recognise UncertifiedLength follows axiolid-evaluate 0.3.15, which no longer lists an offset by distances along an ellipse or a B-spline.
  • The same kernel release snaps a boolean's residues to the scale of the coordinates its result is placed at (axiolid/kernel#312). A cut placed far from the origin, as with georeferenced coordinates, no longer leaves a hairline skin.

[0.26.0] - 2026-10-13 ​

ifc-geometry ​

Changed ​

  • Requires ifc-alignment 0.9 (alignment writers now refuse malformed GlobalIds, #492). No change in lowering. Semver: minor, because the dependency's minor bump changes a public requirement.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.20: linear placements on curve-relation bases derive through curve paths (#418), and stations lower on offset-curve bases (#414), on Axiolid's wave-3 line. Semver: minor, following the dependency.

[0.25.1] - 2026-10-13 ​

ifc-geometry ​

Changed (#439, stations on B-splines trimmed by points resolve in the kernel) ​

Semver: patch. No public item or lowering changes; only the dependency floors of the opt-in compile feature rise. Every product (1230) and every solid, surface and sweep item (2179) of the committed fixtures and references/competitor compiles to the same mesh, bit for bit, or to the same refusal under the old and the new releases.

  • Requires axiolid-mesh-compile 0.3.24, axiolid-construct 0.3.18 and axiolid-mesh-boolean-boolmesh 0.3.5 (and axiolid-evaluate 0.3.14 for the tests).
  • A station on an IfcTrimmedCurve over an IfcBSplineCurveWithKnots trimmed by Cartesian points now resolves through the reference kernel (axiolid/kernel#306). Before, the kernel refused it: its point inversion had no B-spline arm. It now uses the same inversion as PathBound::Point. A derived IfcLinearPlacement on such a trim is the lowered station's frame, bit for bit, so lowering and derivation agree (#436 already derived it).
    • A trim point off the B-spline, or one it passes through twice, is refused by the kernel as PATH_POINT_OFF_CURVE or PATH_POINT_AMBIGUOUS. These are the names the derivation reports as GeometryError::PathPointOffCurve and PathPointAmbiguous.
    • The kernel still refuses a polyline trimmed by points as a station basis (an upstream follow-up). No committed fixture uses one.
  • The kernel's deviation report for runs of sections at stations (IfcSectionedSolidHorizontal, IfcSectionedSurface) is Certified where the frame law along the basis is bounded (axiolid/kernel#247). This crate does not read that report. Of the eight such items that compile in the committed fixtures, one now reports Certified; the other seven stay Unbounded and now name their numerically framed basis. Their meshes are unchanged, as are the three refusals.
  • Swept disks are mitred at corners beside arcs, and closed polylines sweep as loops (axiolid/kernel#248). No committed fixture was refused for either before.

[0.25.0] - 2026-10-13 ​

ifc-geometry ​

Changed (#443, stations along offset runs continued to their basis's ends) ​

Semver: minor (0.x). Stations that were refused now lower, a refusal kind is added to the #[non_exhaustive] SeamWindowRefusal, and refusal texts change. No graph that lowered before changes: every representation item of the committed fixtures and references/competitor (6851 items) lowers to byte-identical wire JSON or to the same refusal.

  • A station whose basis is an IfcOffsetCurveByDistances lowered to CurveRelation::OffsetsAtStations (#429, #433) now lowers. Its stations carry the kernel's seam-snapping window. Examples are an IfcSectionedSolidHorizontal or IfcSectionedSurface section, an IfcPointByDistanceExpression, or an IfcAxis2PlacementLinear on such a curve. Before, these were refused with "the basis curve relation is a parameter-space curve, a surface curve whose p-curve governs, or an instanced curve".

    • The relation is flattened as axiolid-mesh-compile 0.3.23 flattens it as a station basis. A station at the basis's start stands at 0.
    • The span to a station at the basis's end runs open to the end the evaluator measures (#427).
    • A windowed station stands at its own distance where no seam of the basis lies within its window.
    • A derived IfcLinearPlacement on such a curve reads the same path as the compiler, bit for bit.
  • An offset of an offset (IfcOffsetCurveByDistances, IfcOffsetCurve2D or IfcOffsetCurve3D over another offset curve, an OffsetsAtStations included) is refused as a station basis by its own name, as Axiolid refuses it (axiolid/kernel#289). The text names that rule. A trim of an OffsetsAtStations is refused as a trim of an offset.

  • No offset relation reaches the catch-all refusal any more. Its text now names the offset curves Axiolid reads. A basis nested deeper than 64 relations is refused by its own reason.

  • With compile, SeamWindowRefusal::CertificationBudgetExceeded is new. It is returned where the kernel cannot certify a seam to a thousandth of the window within its panel budget (a BudgetExceeded of "certified length panels"). It is reported, like UncertifiedLength, as is_unsupported.

  • Known limit, upstream (TODO(#443)): axiolid-evaluate 0.3.13 does not certify an offset by distances whose law varies along an ellipse or a B-spline. So a windowed station along such a run, past its first stated offset, is refused by the reference kernel:

    • for a sectioned solid, RunSeamWindowRefused { refusal: UncertifiedLength }, which names the runs;
    • for a derived placement, PlacementSeamWindowRefused with UncertifiedLength or CertificationBudgetExceeded.

    The lowered graph itself is correct. With its windows set to zero, the axioval/engine deck compiles to the closed-form prism.

openbim-ifc ​

Changed ​

  • Requires ifc-alignment 0.8 and ifc-geometry 0.19: a Viennese cant bend about a held rail lowers to the banked curve (#364), and derived linear placements on a seam take the incoming tangent (#409), on Axiolid's wave-2 line. Semver: minor, following the dependencies.

[0.24.0] - 2026-10-13 ​

ifc-geometry ​

Fixed ​

  • Requires axiolid-model 0.3.13: geometry graphs written as wire JSON (feature wire) read back bit for bit. 0.3.12 and older parsed about a quarter of doubles one ulp off; payloads already written are unchanged and now read back exactly.

Changed (#436, derived placements on spans inside quadrature-measured curves) ​

  • With compile, a derived IfcLinearPlacement on a trim of an ellipse or a B-spline at an interior parameter, at a point, or at an arc length in its sense from a parameter 0 where its measure starts (the CUBIC transition's IfcPolynomialCurve segment) now derives instead of being refused with PathPieceLengthUnstated. Such a trim reaches the evaluator as a bounded PathPiece (PathBound::Parameter or PathBound::Point in the curve's own coordinates, a stated distance for the arc length; axiolid/kernel#304), which it resolves as the compiler resolves the trim: inside the path it reads bit for bit as the one station::curve_path flattens the lowered basis into, a closed conic's span running across its parameter seam as the compiler's periodic trim does. The derived frame equals the lowered station's, resolved by the reference kernel. A B-spline trimmed by points derives although the reference compiler, which inverts no point on a B-spline, refuses that station; the frame equals its station on the trim by the parameters the points name.
  • A span between distances along a relation (a trim of a composite by parameters) may now reach a piece whose length is a quadrature when it is the relation's last piece and every piece before it is stated: the span is stated, so a derived placement reads it and the station lowering snaps onto its joints exactly, with no window, where before it carried the kernel's seam-snapping window.
  • New GeometryError variants (feature compile): PathPointOffCurve and PathPointAmbiguous for a trim point the evaluator refuses (PATH_POINT_OFF_CURVE, PATH_POINT_AMBIGUOUS), and CurvePathBoundUnsupported for an evaluator that reads no piece bounds (PATH_PIECE_BOUND_UNSUPPORTED), each naming the placement and its basis. PathPieceLengthUnstated now names only what has neither an open nor a bounded form: a span between distances along a relation reaching a quadrature piece other than its last, an arc-length trim of an ellipse or a B-spline against its sense, a varying offset of such a piece, and a trim of a relation by a Cartesian point.
  • The joints of a path with an open or bounded piece are certified only near the placement (CurveEvaluator::path_joint_distances_within, axiolid/kernel#305): every piece before the window, none after it. A piece the evaluator cannot certify after the placement no longer refuses it; one before it still does, as PlacementSeamWindowRefused with UncertifiedLength. The kernel's station resolution also certifies the piece holding the station, so a placement inside such a piece (5 m along a tilted IfcOffsetCurve3D, formerly refused) now derives while its lowered station is refused.
  • The station lowering is otherwise unchanged: it hands the kernel the relation, never a path, and links no evaluator to resolve a bounded piece, so a basis holding one keeps the kernel's seam-snapping window.
  • Requires axiolid-curve 0.3.9, axiolid-model 0.3.12, axiolid-evaluate 0.3.13, axiolid-curve-evaluate-contract 0.3.7 and axiolid-mesh-compile 0.3.23.

Semver: minor. Refused placements now derive, error variants are added, one station lowering now snaps exactly, and the dependency floors rise.

Changed (#433, offset runs to a basis end with quadrature length) ​

  • An IfcOffsetCurveByDistances whose basis is bounded but has no stated length (an offset beside a gradient curve, a spiral, a B-spline or an ellipse, a relation with an ellipse or a B-spline piece, a B-spline or an ellipse itself) now lowers instead of being refused with the former NO_QUADRATURE_LENGTH reason or "states no length". IFC4.3 ADD2 8.9.3.42.3 continues its first and last offsets unchanged to the basis's ends: the run is CurveRelation::OffsetsAtStations ending in RunStation::at_basis_end with the last offsets and, where the first offset stops short of the start, starting with RunStation::at_basis_start (axiolid-model 0.3.12, axiolid/kernel#303). The kernel resolves the end to the basis's length by its own measure. Stations along a basis whose seams are quadratures keep their #429 seam-snapping windows. Such a graph is wire format 1.3. Bases with a stated length keep their stated continuation and emit the identical graph: every representation item of the committed fixtures and the reference corpus lowers to the same wire bytes as before.
  • SeamWindowRefusal::PastBasisEnd (feature compile): the kernel refuses a run that states a station at or past the end it measures, reported as GeometryError::RunSeamWindowRefused naming the run, which ProvenanceMap::seam_window_runs now also lists for runs continued to such an end.
  • Offsets that stop short of an unbounded basis (a line) stay refused, now with a reason naming the unbounded basis.
  • Requires axiolid-model 0.3.12, axiolid-curve 0.3.9, axiolid-evaluate 0.3.13, axiolid-curve-evaluate-contract 0.3.7 and axiolid-mesh-compile 0.3.23.

Semver: minor. Refused offset runs now lower, a refusal kind is added, and the dependency floors rise.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.18: stations on composite, trimmed and station-placed curve bases lower with the seam rule at joints (#346), and curve segments placed by IfcAxis2PlacementLinear lower to stations (#311, part). Semver: minor, following the dependency.

[0.23.0] - 2026-10-12 ​

ifc-geometry ​

Changed (#429, runs of sections along bases with quadrature joints) ​

  • An IfcSectionedSolidHorizontal or IfcSectionedSurface along a directrix whose seams lie at arc-length integrals (an offset beside a gradient curve, a spiral, a B-spline or an ellipse, a relation with an ellipse or a B-spline piece, a B-spline corner knot) now lowers instead of being refused with the former RUN_UNLOCATED reason. Each section stays at its own distance and carries the model's precision as Axiolid's seam-snapping window, reading SeamSide::Incoming (SectionAtStation::with_seam_window / with_seam_side, axiolid-model 0.3.11, axiolid/kernel#301). The kernel snaps it onto the seam and mitres there at half angle (8.8.3.35.1); such a graph is wire format 1.2. Runs along bases whose seams are stored data keep their exact snapping and emit the identical graph (wire 1.0), as checked on the committed and reference corpora.
  • GeometryError::RunSeamWindowRefused { product, runs, stations, refusal } (feature compile) names the run when the kernel refuses its windows: two seams in a window, a seam on its edge, a length it cannot certify, or the new SeamWindowRefusal::NotIncreasing (positions that no longer increase strictly once snapped). ProvenanceMap::seam_window_runs lists the windowed runs.
  • An IfcOffsetCurveByDistances along such a basis is still refused, now by a reason naming #433: IFC continues its offsets to the basis's end, whose distance is a quadrature this crate does not compute. Its stations are built as RunStations; with a window they would lower to CurveRelation::OffsetsAtStations, and without one they still lower to OffsetByStations, unchanged.
  • A reversal at a quadrature seam within a run is refused by the kernel's mitre, which this crate cannot see.
  • Requires axiolid-model 0.3.11, axiolid-curve 0.3.8, axiolid-evaluate 0.3.12, axiolid-curve-evaluate-contract 0.3.6, axiolid-reference 0.3.8 and axiolid-mesh-compile 0.3.22.

Semver: minor. Refused runs now lower, an error variant and a refusal kind are added, and the dependency floors rise.

Changed (#427, derived placements on offsets whose length is a quadrature) ​

  • With compile, a derived IfcLinearPlacement on an offset whose length is a quadrature now derives instead of being refused: an IfcOffsetCurveByDistances beside an IfcGradientCurve (a kerb beside an alignment), an offset beside a clothoid or another spiral, offsets of B-splines and ellipses, a variable offset of a circle, and a relation with a whole ellipse or B-spline piece. Such a piece reaches the evaluator as an open-ended PathPiece (PathPiece::whole, axiolid/kernel#298), its end resolved to the evaluator's own length, so the path reads bit for bit as the stated path the kernel flattens the same relation into. The offset's base is split at every seam of its curve the kernel splits it at (chain joins, curvature seams, every profile break), so the pieces are the kernel's. basis_curve_path returns such a path.
  • The joints after an open piece are located by the evaluator's path_joint_distances, certified to a thousandth of the window the station lowering hands the kernel (the model's precision, capped at 1 mm, or rounding). A DistanceAlong whose window holds exactly one joint (or a stated piece's seam after one) reads it from the incoming piece (IFC4.3 ADD2 8.9.3.48.3, #409), at the evaluator's own point of the joint; the result equals the lowered station's frame resolved by the reference kernel, and CachedPositionPolicy::Verify compares a cache against it. A segment of a basis placed at a windowed station along such a basis is read the same way, within the station's own window.
  • GeometryError::PathPieceLengthUnstated now names only spans that start inside a curve whose measure is a quadrature (a trim of an ellipse or a B-spline at an interior parameter, by an arc length or a point, a span between stations along a relation with such a piece), which no open piece expresses. Its message no longer cites #427.
  • The same axiolid floors as #429 above; with feature wire, wire::FORMAT_VERSION is 1.2. A derived placement writes no graph.

Added (#427) ​

  • With compile: GeometryError::CurvePathOpenEndUnsupported { placement, basis }, an evaluator that reads stated spans only refusing an open piece (OPEN_PATH_END_UNSUPPORTED); GeometryError::PathJointDistancesUnsupported { placement, basis }, an evaluator that does not certify joints (PATH_JOINT_DISTANCES_UNSUPPORTED); and GeometryError::PlacementSeamWindowRefused { placement, basis, distance, refusal }, the placement's window holding two seams, a seam straddling its edge, or a joint the evaluator cannot certify (SeamWindowRefusal::Ambiguous, Straddled, UncertifiedLength), as the kernel refuses the same station. Each is typed; nothing falls back to an estimate.

Semver: minor while 0.x: placements that were refused now derive, a refusal narrows and error variants are added.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.17: polygonal half-space boundaries with circular-arc segments lower exactly (#398), on Axiolid's 0.3.16 line. Semver: minor, following the dependency.

[0.22.1] - 2026-10-12 ​

ifc-geometry ​

Fixed (axiolid/kernel#297) ​

  • The wire feature requires axiolid-model 0.3.10, whose writer labels each payload with the lowest wire format version its content needs: 1.0 unless a station carries a seam-snapping window, then 1.1. Window-free payloads are labelled 1.0 again, byte-identical to what axiolid-model 0.3.8 writes and readable by it; 0.3.9 labelled every payload 1.1.

Semver: patch.

[0.22.0] - 2026-10-10 ​

ifc-geometry ​

Changed (#423, stations on offsets whose length is a quadrature) ​

  • A station (IfcPointByDistanceExpression, IfcAxis2PlacementLinear, and an IfcCurveSegment placed at one) on a basis whose seams lie at arc-length integrals now lowers instead of being refused: an offset beside a gradient curve (a kerb beside an alignment), a spiral, a B-spline or an ellipse, a variable offset of a circle, a relation with an ellipse or a B-spline piece, and a B-spline with a corner knot. It is an OrientedCurveStation at its own distance, reading SeamSide::Incoming, with the model's precision (capped at 1 mm) as its seam-snapping window (axiolid-model 0.3.9, axiolid/kernel#294): the kernel reads it on a seam within the window from the previous segment (IFC4.3 ADD2 8.9.3.48.3), at the seam's certified distance. A station on a basis whose seams are stored data keeps this crate's own exact snapping, so its graph is unchanged (no window, 1.0 content): a run's stations carry no window and need that snapping anyway.
  • A run of sections or offsets (IfcSectionedSolidHorizontal, IfcSectionedSurface, IfcOffsetCurveByDistances) along a basis whose seams are quadratures is refused by name (it was refused before by the basis's reason).
  • axiolid-model floor 0.3.9, axiolid-curve-evaluate-contract 0.3.5, axiolid-mesh-compile 0.3.20 (feature compile-reference-backend); all additive. With feature wire, wire::FORMAT_VERSION is 1.1. An envelope carries the lowest wire version its content needs: 1.0, or 1.1 when a station carries a seam-snapping window; axiolid-model 0.3.9 labels every payload 1.1 (axiolid/kernel#297), and a reader on axiolid-model 0.3.8 or older refuses 1.1.

Added ​

  • ProvenanceMap::seam_windows: the stations lowered with a window.
  • With compile: GeometryError::StationSeamWindowRefused { product, stations, refusal } and compile::SeamWindowRefusal (Ambiguous, Straddled, UncertifiedLength): the kernel's refusal of a window (two seams in it, a seam whose certified position straddles its edge, a seam it cannot certify) names the stations, where it was CompilationRefused naming the product.
  • With compile: GeometryError::PathPieceLengthUnstated { placement, basis }: a derived IfcLinearPlacement on such a basis is refused by name, since the evaluator's CurvePath states each offset piece's span in its own length, which only an execution provider computes (#427, axiolid/kernel#298); it was Unsupported with the station lowering's reason.

Semver: minor while 0.x: stations that were refused now lower, a refusal changes variant, and the dependency floors rise.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.16: stations on seams lower with the incoming tangent and runs mitre across seams (#346, part 1), on Axiolid's 0.3.15 line; the backend comparison example (#31). Semver: minor, following the dependency.

[0.21.0] - 2026-10-10 ​

ifc-geometry ​

Added (#367, the neutral graph as a value) ​

  • Feature wire (off by default): ifc_geometry::wire, a re-export of axiolid_model::wire (Axiolid's versioned wire format of a GeometryGraph, Axiolid ADR 0085: FORMAT_NAME, FORMAT_VERSION 1.0, FormatVersion, WireError), and with it GeometryGraph::to_json, from_json, to_cbor and from_cbor, through axiolid-model's serde feature. It encodes the graph lowering produces and computes nothing (ADR 0004).

Changed ​

  • axiolid-model floor 0.3.8 (axiolid/kernel#267), which brings axiolid-core 0.3.2, axiolid-curve 0.3.7, axiolid-mesh 0.3.3, axiolid-primitive 0.3.3, axiolid-profile 0.3.2, axiolid-surface 0.3.3, axiolid-topology 0.3.2 and axiolid-linear 0.3.2; all additive.

Semver: additive (a new opt-in feature), a minor release while 0.x, since the dependency floors rise.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.15: collinear IfcArcIndex segments lower as straight segments everywhere (#396), and geometry where-rules follow the declared release, with IFC2X3 WRnn names and the release-only rules (#397, #400, #402). Semver: minor, following the dependency.

[0.20.0] - 2026-10-10 ​

ifc-geometry ​

Semver: a minor release. Behaviour changes: a linear placement whose basis curve is a curve relation, refused by name until now, derives through a CurveEvaluator, and an evaluator without curve paths is refused there by a new GeometryError variant (the enum is #[non_exhaustive]); stations and derived placements along offset curves, refused until now, lower and derive; an IfcOffsetCurve2D lowers with a reference direction where it carried none, a change in the lowered graph. A public function is added. The dependency floors rise with the workspace.

Added ​

  • A derived IfcLinearPlacement on a curve-relation basis (#418): a plain IfcCompositeCurve, an IfcTrimmedCurve, or a composite of IfcCurveSegments placed by IfcAxis2PlacementLinears. The basis is read as Axiolid's neutral CurvePath (axiolid/kernel#290), built from the stored relation the station lowering reads (#346), each segment's station framed through the caller's evaluator; the frame comes from CurveEvaluator::path_frame_at off a joint and path_frame_at_on(.., SeamSide::Incoming) within precision of one, as #409 reads a seam. It equals the lowered station's frame on and off joints, and CachedPositionPolicy::Verify accepts a cache computed with the incoming segment and refuses one computed with the outgoing segment. An IfcParameterValue along such a basis and a composite whose pieces do not meet are refused by name.
  • constraint::placement::derive::basis_curve_path (compile feature): the CurvePath a derivation reads along a curve-relation basis. A test pins it equal to the path axiolid-mesh-compile's station::curve_path builds from the lowered graph.
  • GeometryError::CurvePathUnsupported (compile feature) names the placement and its basis curve when a caller's evaluator does not implement curve paths and refuses them with CURVE_PATH_UNSUPPORTED. A frame read on one piece is never used in its place. is_unsupported() is true for it.
  • Stations on offset-curve bases (#414): an IfcPointByDistanceExpression, an IfcAxis2PlacementLinear, and runs of sections and offsets whose basis curve is an IfcOffsetCurve2D, an IfcOffsetCurve3D or an IfcOffsetCurveByDistances, nested in composites and placements too, lower onto Axiolid's offset station basis (axiolid/kernel#289, Axiolid ADR 0082 amendment 2026-10-10). A distance runs along the offset's OWN length, not its basis's. The offset's joints, one per seam of its basis (every polyline vertex, every joint of a composite) and per station of an offset by distances, are read from the stored relation as the compiler flattens it (lower::station::relation::offset): beside a line or a polyline edge a line, constant or linear between stations, of length sqrt((L + dg)^2 + dl^2 + dv^2); beside a circle, constantly in its plane, a circle of radius r - a, (r - a) / r times the arc's length. A station within the model's precision of a joint stands on it reading SeamSide::Incoming, the previous segment (IFC4.3 ADD2 8.9.3.48.3). The joints and lengths are pinned to the kernel's own within ARC_LENGTH_TOLERANCE: station::seams where the kernel states them exactly, the composite's seams along station::curve_path where it measures a polyline offset by quadrature. A derived IfcLinearPlacement on an offset basis reads it as a CurvePath of PathCurve::Offset pieces (#418's path builder), equal to station::curve_path's and to the lowered station's frame on the kerb's corner.
  • synthetic-lowering/station_offsets_ifc4x3.ifc: an IfcOffsetCurve2D of a line and a tangent quarter circle (a joint whose sides meet, an arc of radius 9), of a quarter circle (radius 11, 5.5 pi m), an IfcOffsetCurveByDistances of an L polyline vanishing at its corner, a section run and an offset run along offsets, and an IfcOffsetCurve2D across the L's corner; tests through the reference kernel on and 4 um past each joint.

Changed ​

  • Requires axiolid-curve 0.3.6, axiolid-curve-evaluate-contract 0.3.4 and, behind compile, axiolid-mesh-compile 0.3.19 (and axiolid-evaluate 0.3.10 for the tests): the releases that add curve paths (axiolid/kernel#290) and measure a station along an offset (axiolid/kernel#289). The lock also moves axiolid-nurbs 0.3.6, axiolid-brep-boolean 0.1.9 and axiolid-overlay 0.3.12. Every other test passes unchanged against them.
  • A closed conic trimmed across its parameter seam inside a relation basis is located from its start parameter taken modulo one turn, as Axiolid locates it, so its span starts within the curve's measure.
  • The refusal of a basis curve the derivation does not read names the composite and trimmed curves among those it does.
  • An IfcOffsetCurve2D lowers to CurveRelation::Offset with its frame's +Z as reference_direction, where it carried None (#414). Every curve lowers to a 3D curve, a 2D one in z = 0 of its frame, and IFC's positive Distance "in the sense of an anti-clockwise rotation through 90 degrees from the tangent vector T" (8.9.3.40) is Z x T there, the 3D offset's V x T (8.9.3.41) and Axiolid's left lateral: the same curve. Axiolid refuses a planar offset law on a 3D curve as a station basis, so None left nothing measurable along it. Nothing meshed an offset relation outside stations, so no compiled result changes.
  • The refusals of offset bases now name what the kernel refuses: an offset of an offset, a trim of an offset curve (it takes its basis's parameterisation, not its own length), an offset through a circle's centre, and an offset whose length is a quadrature (beside an ellipse, a B-spline, a spiral or a gradient curve, or a variable offset of a circle). An offset across a corner of its basis, whose sides do not meet (IFC: the basis "shall have a well-defined tangent direction at every point"), and a 3D offset whose tangent runs along its reference direction lower, and the kernel refuses them by name, typed. The dispatch ledger's IfcPointByDistanceExpression rows say so.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.14: IfcPolygonalBoundedHalfSpace boundaries given as IfcCompositeCurve or IfcIndexedPolyCurve with straight edges lower (#393). Semver: minor, following the dependency.

[0.19.0] - 2026-10-09 ​

ifc-geometry ​

Semver: a minor release. Behaviour changes on seams: a linear placement derived through a CurveEvaluator on a tangent discontinuity takes the incoming tangent, and an evaluator that cannot read that side is now refused there. GeometryError gains a variant (the enum is #[non_exhaustive]). The dependency floors rise with the workspace (#364, below); the seam sides are axiolid/kernel#286 in the same releases.

Changed ​

  • Requires axiolid-curve 0.3.5, axiolid-curve-evaluate-contract 0.3.3 and, behind compile, axiolid-mesh-compile 0.3.17 (and axiolid-evaluate 0.3.9 for the tests): the releases that rotate a banked curve about a held rail (axiolid/kernel#279), which ifc-alignment now lowers a Viennese bend onto. Nothing here builds a banked curve; every test passes unchanged against them.
  • A derived IfcLinearPlacement on a tangent discontinuity takes the incoming tangent (#409), as IFC4.3 ADD2 8.9.3.48.3 asks ("the tangent of the previous segment governs") and as the lowered station has since #346. derive_placement_transform locates the seams as lower::station does, from the basis curve's stored data, snaps a DistanceAlong within the model's precision (capped at 1 mm) to the seam's distance, and asks the evaluator for CurveEvaluator::frame_at_on(.., SeamSide::Incoming) there (axiolid/kernel#286). A polyline parameter on a vertex is read at the vertex's distance. Off a seam nothing changes. Before, the derived frame on a grade break or a polyline corner was the outgoing one, so it disagreed with the same station lowered as geometry, and CachedPositionPolicy::Verify refused a cache an exporter computed with the incoming tangent while accepting one computed with the outgoing tangent; now the reverse holds.

Added ​

  • GeometryError::SeamSideUnsupported (compile feature) names the placement, its basis curve and the seam's distance when a caller's evaluator does not implement seam sides and refuses Incoming with SEAM_SIDE_UNSUPPORTED. The outgoing frame is never used in its place. is_unsupported() is true for it.

Documentation ​

  • The derived frame's documentation states the evaluator's axes as kernel#242 documents them (x tangent, y up, z right); the derivation reads only the tangent, and a test pins the IFC frame (tangent, left, up) to (x, -z, y) on and off a seam.

openbim-ifc ​

Changed ​

  • Requires ifc-geometry 0.13: net bodies (lower_product_net_with) are subtracted in the host's frame under one world Instance, so flush openings in georeferenced models mesh (#388). Semver: minor, because the re-exported neutral graph changes shape.

[0.18.0] - 2026-10-09 ​

ifc-geometry ​

Semver: a minor release. No public item is added, removed or changes signature, but behaviour changes: stations along curve relations (#346) and IfcCurveSegments placed by an IfcAxis2PlacementLinear (#311), refused by name until now, lower. The Axiolid requirements are the ones #398 raised (axiolid-model 0.3.7, axiolid-mesh-compile 0.3.16).

Added ​

  • Stations along curve relations (#346). Axiolid measures a station along a composite, a trim, a surface curve whose 3D curve governs and a curve placed at a station since axiolid-model 0.3.7 (axiolid/kernel#285, ADR 0082 amendment): end to end through the pieces, each in its own curve's convention, every joint a seam. So an IfcPointByDistanceExpression, an IfcAxis2PlacementLinear and the runs of IfcSectionedSolidHorizontal, IfcSectionedSurface and IfcOffsetCurveByDistances on a plain IfcCompositeCurve, an IfcTrimmedCurve or a composite of segments placed at stations lower. On a joint, and within the model's precision of one, a station stands on the joint reading SeamSide::Incoming, the previous segment, as IFC4.3 ADD2 8.9.3.48.3 asks. The joints are read from the stored relation (lower::station::relation, repeating the compiler's flattening, since axiolid-evaluate is an execution provider this crate does not link): pieces' lengths from a line's direction, a circle's radius, a polyline's edges and the measure of a curve parameterised by it; trims by parameter, by arc length, or by a point on a line or a circle, a closed conic's within one turn; a segment whose sense disagrees reversed. The tests pin them against the kernel's own seams.
  • An IfcCurveSegment placed by an IfcAxis2PlacementLinear (#311) lowers to an InstanceAtStation: the parent's piece in local coordinates (start at the origin, tangent along x) placed in the frame of the OrientedCurveStation the placement lowers to, whose local x, y, z are the station's tangent, left and up as IFC4.3 ADD2 8.9.3.4 reads them. A composite of such segments is measured in the segments' own arc length, whatever base they stand on. Part of #311: IfcSegmentedReferenceCurve stays refused (below).
  • synthetic-lowering/station_relations_ifc4x3.ifc: a plain composite of a polyline, a polyline stored backwards (SameSense false) and a quarter circle trimmed by Cartesian points, with points on its joints and 4 um past one; a composite with a gap; and two IfcLine segments placed at stations 10 and 30 of a gradient curve. Tests resolve them through the reference kernel against closed forms.

Changed ​

  • IfcSegmentedReferenceCurve's refusal (dispatch::PLANNED) names what remains open in #311 precisely: its segments are curves in the (distance along, deviating elevation) space of its base curve, and no normative rule maps a segment's ParentCurve and placement Axis to a cant law and pivot; implementations differ in the coefficient scaling and the cross-slope formula.
  • An IfcCurveSegment in an IfcGradientCurve placed by an IfcAxis2PlacementLinear keeps its refusal, now naming why: a gradient curve's segments are read as plan and profile pieces, which need a resolved placement.
  • Uses the Axiolid releases #398 raised the workspace to (axiolid-model 0.3.7, axiolid/kernel#285 for stations along relations). Upstream now refuses a tilted placement of a plan-measured curve only, reading a tilted arc-length one in its own reference-up frame; nothing lowered before reached that rule, and no test's result changed with the bump alone.

Refused by name ​

  • A station along an offset curve (IfcOffsetCurve2D/3D), which Axiolid measures no station along; along a relation joining a gradient curve with arc-length pieces, through which no one distance runs; and along a relation with an ellipse or a B-spline piece, or a trim whose ends are not stated by its data, whose joints lie at an arc-length integral. A composite whose pieces do not meet, or whose undeclared sense runs a piece backwards, is refused by the kernel.

openbim-ifc ​

Added (#342, declared attribute types) ​

  • Behind schema-api: declared_type(schema, type_text) resolves an EXPRESS type token against a release's tables to a DeclaredType: Simple (SimpleType: INTEGER, REAL, NUMBER, STRING, BOOLEAN, LOGICAL, BINARY, widths dropped), Entity, Enumeration (with its items), Defined (with what it aliases, an aggregate included: IfcCompoundPlaneAngleMeasure), Select (every member resolved, nested SELECTs included), Aggregate, or Unresolved for a token the tables do not declare; bounded, so a cyclic alias ends Unresolved. attribute_type(schema, entity, name) gives an attribute's slot and declared type with its own aggregation levels outermost.

Semver: additive, a patch release.

[0.17.0] - 2026-10-09 ​

ifc-geometry ​

Semver: a minor release. No public item is added, removed or changes signature, but behaviour changes as it did for #393: half-space boundaries with circular arcs, refused as Unsupported until now, lower, so a consumer matching on the boundary node now meets a Profile as well as a Curve2; PARTIAL loses the family's two rows; and the Axiolid requirements rise (axiolid-model 0.3.7, axiolid-evaluate 0.3.8, and with compile-reference-backend axiolid-mesh-compile 0.3.16 and axiolid-construct 0.3.17).

Added ​

  • IfcPolygonalBoundedHalfSpace boundaries with circular-arc segments (#398): an IfcCompositeCurve with trimmed IfcCircle segments, or an IfcIndexedPolyCurve with a non-collinear IfcArcIndex, lowers to a Profile::Contour boundary (no holes) whose arcs are exact Circle2 segments, never chords. SolidOperation::BoundedHalfSpace takes a profile boundary since axiolid-model 0.3.7 (axiolid/kernel#277, Axiolid ADR 0084): the reference exact compiler clips by a right circular cylinder per arc, and the mesh compiler flattens the arcs under its chord budget with a certified deviation. A boundary of straight edges still lowers to the same closed Polyline2 as before, so its output is unchanged. Still refused, by name: as Degenerate, a gap at an arc's end (judged within the model's Precision), an open curve, a point off the plane and an IfcArcIndex with coincident points; as Unsupported, a boundary circle placed by an IfcAxis2Placement3D, an IfcCurveSegment member, a reparametrised segment and any other curve family. A boundary that crosses or touches itself is the kernel's to refuse (InvalidInput, named alike in both compilers); it reaches the caller as CompilationRefused. IfcPolygonalBoundedHalfSpace is now listed as implemented, with no variant rows. halfspace_boundaries_ifc4x3.ifc gains two walls, one clipped by a six-segment composite with two arcs of radius 1.2, one by an indexed boundary with an IfcArcIndex; each compiles, exact and meshed, to its closed-form volume.

openbim-ifc ​

Added (#328, geometry in the bindings, ADR 0021) ​

  • Behind geometry-select: product_placements(model, ids) -- per product with a shape (or per id given), its world placement (products_world_transforms, metres) and the Body representation select_shape_representation chooses, as ProductPlacement { product, world, body }, each half its own GeometryResult -- with SelectedRepresentation (identifier, type, context) and SelectedContext (context type, derived from the parent for a sub-context; identifier; target view literal). column_major(&Transform) gives the 4x4 column-major matrix graphics APIs read.
  • Feature mesh (geometry plus ifc-geometry's compile-reference-backend; never default, ADR 0004): product_meshes(model, ids), one result per product: ProductMesh { world, positions, indices }, f32 positions relative to the world placement (mapped back through its inverse in f64), u32 indices, compiled with one reference backend at a one-millimetre tolerance; a product without a Body has empty arrays, a refused one its GeometryError. compiled_features() reports mesh.

Changed ​

  • full now includes mesh, and so links the reference compile backend.

Semver of #328: additive; mesh needs the compile re-exports of the next ifc-geometry (0.11.0).

[0.16.0] - 2026-10-09 ​

ifc-geometry ​

Semver: a minor release (0.16.0). No public item is added, removed or changes signature, but behaviour changes (#346): stations on a tangent discontinuity and runs of sections and offsets across one, refused by name until now, lower (an IfcPointByDistanceExpression on a seam to an OrientedCurveStation, the only station that carries a seam side, where one off a seam stays a CurveStation); and the Axiolid requirements rise (axiolid-model 0.3.6, axiolid-curve 0.3.4, and with compile-reference-backend axiolid-mesh-compile 0.3.15, axiolid-mesh-compile-contract 0.3.3 and axiolid-construct 0.3.16). The example below and the compile module's new section alone would be a patch.

Added ​

  • examples/backend_compare/ (#31; required-features = ["compile"]) compiles a fixed corpus of committed fixtures through every backend in one list, with no cfg in the traversal. The list holds the reference backend under compile-reference-backend, and polygon-extruder, a MeshCompiler defined in the example that names only the published contracts and builds under compile alone. polygon-extruder compiles collections and instances, triangle and polygon meshes, faceted B-reps, extrusions of polygonal profiles with holes, and blocks, and refuses everything else with GeomError::UnsupportedInput. The example prints:

    • wall time per fixture, keyed by BackendId: the first pass reported on its own, then the median and range of --iterations passes;
    • mesh size (vertices, triangles) and coverage (meshed, refused, no body) per fixture;
    • agreement on signed volume, area and bounding box within a stated relative tolerance, every divergence named by fixture, product, metric and both values;
    • every product one backend meshed and the other refused.

    The method, machine, profile and load are printed beside the numbers, which are not a cross-kernel performance claim. scripts/gate.sh runs it quick under both features and with --strict beside the reference. The compile module docs and ADR 0012 (amendment) link it.

  • Stations on tangent discontinuities (#346). IFC4.3 ADD2 8.9.3.48.3: "If DistanceAlong coincides with a point of tangential discontinuity (within precision limits), then the tangent of the previous segment governs." Axiolid now states a seam side (SeamSide, axiolid-model 0.3.6, ADR 0082 amendment, axiolid/kernel#263), so such a station lowers with SeamSide::Incoming: an IfcAxis2PlacementLinear as an OrientedCurveStation reading the incoming side, an IfcPointByDistanceExpression as CurveStation::with_seam_side's unturned oriented station. OffsetLongitudinal then runs along the incoming tangent.

  • Runs across tangent discontinuities (#346): IfcSectionedSolidHorizontal, IfcSectionedSurface and IfcOffsetCurveByDistances whose stations span a polyline corner or a grade break lower, and Axiolid cuts them in the half-angle mitre plane 8.8.3.35.1 and 8.8.3.37.1 ask for (the plane normal to the bisector of the two tangents). IFC states no join for the offset curve; the mitre is the one the sweeps on the same stations take.

  • synthetic-lowering/station_seams_ifc4x3.ifc: a gradient curve with a grade break and an L-shaped polyline, with points on and within the precision of each seam, a sectioned solid and an offset curve (and a disk swept along it) across the corner, and a sectioned surface across the grade break. Tests resolve them through axiolid-reference and the reference mesh compiler against closed forms: the incoming tangent, the mitre's corners, the volume and the area.

Changed ​

  • A station within the model's declared Precision of a seam (capped at 1 mm, or 1e-9 * max(1, |s|) where larger) is stored at the seam's own distance (#346). Axiolid reads a station on a seam only within ARC_LENGTH_TOLERANCE * max(1, s) (1e-12); snapping to the distance Axiolid itself reads from the same stored data (pinned against exact_station_seams in the tests) makes the two rules agree, where a station 4 um off a corner would otherwise be on the corner for IFC and on one leg for the kernel. A station of a run is snapped the same way, so its section stands in the mitre plane; two that snap onto one seam are refused as out of order.
  • The seam reader carries whether the curve turns back on itself at a seam, by Axiolid's MITRE_TOLERANCE (1e-6 on the cosine of half the turn), repeated in lower::station::seams because axiolid-evaluate is an execution provider this crate does not link (ADR 0004).
  • Workspace requirements: axiolid-model 0.3.6, axiolid-curve 0.3.4, axiolid-evaluate 0.3.7 (dev), axiolid-mesh-compile 0.3.15, axiolid-mesh-compile-contract 0.3.3 and axiolid-construct 0.3.16. The kernel now reads a station within ARC_LENGTH_TOLERANCE of a seam at the seam; no test's result changed with the bump.

Refused by name ​

  • A run of sections or offsets across a seam where the basis turns back on itself: "very sharp edges may result in nearly impossible miter" (8.8.3.35.1), and the half-angle plane has no extent there.
  • Still: a station on a basis whose seams cannot be read from stored data. A curve relation (a plain IfcCompositeCurve, a trimmed or offset curve): Axiolid resolves no station along one, which remains open in #346. A B-spline with a corner knot, whose distance is an arc-length integral (Axiolid's exact_station_seams refuses it too).

openbim-ifc ​

Changed ​

  • spatial_properties and the property-edit planner (apply_property_edits, stage_property_edits) resolve through one ifc_properties::PropertyIndex of the model instead of calling the exact resolver per element or per edited object, so listing every element's properties, or a batch editing many objects, is linear in the model (#352). Answers and refusals are unchanged. ifc::properties::PropertyIndex is reachable through the properties feature.
  • Behind geometry, the re-exported ifc_geometry gains, under geometry::lower, lower_product_net_with and NetOptions (#351): a Reference View opening with only a Reference representation can be taken as already applied to its host's Body, listed in NetLowering::taken_as_applied. With ifc-geometry's compile feature it also gains the evaluator-taking linear placement (#353, LoweringSession::with_curve_evaluator, product_world_transform_with_evaluator) and the cached-position check (#354, CachedPositionPolicy), and the derived frame follows IFC4.3 (#355). When ifc-geometry 0.10 is released, the re-export moves to it, which makes the next release here a minor one.

Semver of #352 alone: no public signature changes, a patch release.

[0.15.0] - 2026-10-09 ​

ifc-geometry ​

Semver: an IfcIndexedPolyCurve whose collinear IfcArcIndex was refused as Degenerate now lowers (#396), and rules::validate no longer reports BoundaryType for an IFC4X3 IfcIndexedPolyCurve boundary or a subtype of IfcCompositeCurve (#397). Every where-rule is now checked in the file's declared release and reported under that release's name (#400): an IFC2X3 file's violations are named WR1, WR31, ... as IFC2X3 TC1 names them, and the set of violations changes for IFC2X3 and IFC4X3 files as listed below. No public item is added, removed or changes signature (rules::validate, rules::validate_model, rules::placement::check, rules::solid::check and RuleViolation are as before); a consumer matching IFC2X3 violations by their IFC4 rule name must match the WRnn name instead. Behaviour changes only, so the next release is a minor one (0.15.0); nothing is breaking at the API level. No committed fixture's violation set changes. The where-rules of the geometry entities IFC4 does not declare (#402) add violations only, on files that break them, and public API is unchanged, so they join the same minor release.

Added ​

  • rules::validate and rules::validate_model check the where-rules of the geometry entities IFC4 ADD2 TC1 does not declare, each in the releases that declare it, under that release's name and text (#402), and the DECLARED table lists them so the schema-backed tests hold them to each bundled release:

    • IFC2X3 TC1 Ifc2DCompositeCurve.WR1 (ClosedCurve) and WR2 (Dim = 2); IfcRationalBezierCurve.WR1 (one weight per control point) and WR2 (IfcCurveWeightsPositive);
    • IFC4X1 on: IfcSectionedSolid.ConsistentProfileTypes, DirectrixIs3D and SectionsSameType; IfcSectionedSolidHorizontal.CorrespondingSectionPositions and NoLongitudinalOffsets (IFC4X1 and IFC4X2 read the offset on the IfcDistanceExpression position, IFC4X3 ADD2 on the position's IfcPointByDistanceExpression location); IfcTriangulatedIrregularNetwork.NotClosed (a written Closed = TRUE violates; an omitted one is UNKNOWN and conforms);
    • IFC4X3 ADD2: IfcAxis2PlacementLinear.WR1 (the location is an IfcPointByDistanceExpression) and WR2 (Axis not parallel to RefDirection, read as AxisToRefDirPosition is); IfcPolynomialCurve.CorrectPositionDim and ValidCoefficients; IfcSectionedSurface.AreaProfileTypes (at least one CURVE cross-section, as its text demands), CorrespondingSectionPositions, DirectrixIs3D, NoOffsets and SectionsSameType.

    #402 listed the sectioned solids and the TIN as IFC4X3-only; IFC4X1 and IFC4X2 declare them too, and are checked. IfcCurveWeightsPositive is transcribed once (rules/express.rs) for both the Bezier curve and IFC4's IfcRationalBSplineCurveWithKnots.WeightsGreaterZero: its Weights array is IfcListToArray(...) = ? when WeightsData and ControlPointsList differ in length, and the function then returns TRUE, so a rational B-spline with mismatched lists now reports only SameNumOfWeightsAndPoints, not also WeightsGreaterZero. Dim follows IfcPointDim (IFC4X3 ADD2) and the Dim of IfcPointOnCurve and IfcPointOnSurface (every release): a point's BasisCurve or BasisSurface, and an IfcPointByDistanceExpression's BasisCurve, where it was undecided.

Changed ​

  • The general curve lowering (lower_curve_node, and every representation item, axis or directrix that reaches it) treats a collinear IfcArcIndex as IFC4 ADD2 TC1 and IFC4X3 ADD2 prescribe: "The three points shall not be co-linear. In case that this informal proposition is not maintained, the arc segment shall be treated as a polyline segment" (#396). Three distinct points collinear within the model's Precision lower as a straight Curve3::Polyline segment of the composite, start -> end when the middle point lies between the others and start -> mid -> end when it does not; they were refused as Degenerate. Two points that coincide within Precision are still Degenerate. Both tests now use the model's Precision (in the curve's own coordinates, 1.E-5 project units when none is declared) instead of an absolute f64::EPSILON bound on a squared area, and they are one helper, constraint::tolerance::arc_points, shared with the profile and half-space boundary reader of #335 and #393, whose results are unchanged except that its tolerance, like every point comparison's, is floored at floating-point rounding for very large coordinates. A model whose declared Precision is not a finite positive number now refuses an indexed curve with an arc segment, as it refuses an indexed profile boundary. The fixture indexed_curve_arcs.ifc (IFC4) lowers representation curves with a collinear, an out-and-back collinear, a nearly collinear and a genuine arc, and halfspace_boundaries_ifc4x3.ifc gains a wall whose boundary has a collinear arc; both pass the dispatch corpus.

Fixed ​

  • The where-rule IfcPolygonalBoundedHalfSpace.BoundaryType is checked in the file's own release (#397). IFC4X3 ADD2 admits IfcPolyline, IfcCompositeCurve and IfcIndexedPolyCurve; IFC2X3 TC1 (WR42), IFC4 ADD2 TC1, IFC4X1 and IFC4X2 admit only the first two. An IFC4X3 file with an indexed boundary was flagged WrongType. The release is the header's FILE_SCHEMA, IFC4 ADD2 TC1 when none known is declared, and, as TYPEOF includes supertypes, a subtype of an admitted type (an IfcBoundaryCurve) satisfies the rule, judged in that release's entity table.
  • Every where-rule in rules is checked against the declared release's own text (#400). One helper, rules/release.rs, reads the release from FILE_SCHEMA (IFC4 ADD2 TC1 when none known is declared), lists which rules each bundled release declares on which entity, and answers every IN TYPEOF test in that release's entity table; no rule matches names by hand any more. Two tests hold it to the schemas: the rule list must equal each bundled release's own table, and each rule's EXPRESS text in every release (from references/ifc-spec) must be IFC4's except where the code branches on and cites the difference.
    • IfcBooleanClippingResult.FirstOperandType: IFC2X3 TC1 (WR1) admits only an IfcSweptAreaSolid or IfcBooleanClippingResult, so an IFC2X3 swept disk is now reported. IFC4 ADD2 TC1 to IFC4X3 ADD2 spell the third disjunct IFCSWEPTDISCSOLID, which names no entity; it is read as IfcSweptDiskSolid, as buildingSMART/IFC4.x-development#927 and its open correction #1107 state, so a clipped swept disk stays admitted.
    • Rules IFC2X3 TC1 does not declare are no longer applied to IFC2X3 files: IfcBooleanResult.FirstOperandClosed/SecondOperandClosed, IfcDirection.MagnitudeGreaterZero, IfcRepresentationMap.ApplicableMappedRepr, and DirectrixBounded.
    • IfcRevolvedAreaSolid.AxisStartInXY in IFC4X3 ADD2 also requires Axis.Location to be an IfcCartesianPoint; IFC4X3's LocationIsCP on IfcAxis1Placement, IfcAxis2Placement2D and IfcAxis2Placement3D is now checked too.
    • DirectrixBounded in IFC4X3 ADD2 is declared on IfcDirectrixCurveSweptAreaSolid and so binds every subtype, including IfcDirectrixDerivedReferenceSweptAreaSolid, which was not checked.
    • IFC2X3 TC1's IfcSweptSurface.WR1 (no IfcDerivedProfileDef as the swept curve), dropped in IFC4, is checked for IFC2X3 files.
    • A violation names its rule as the declared release does (WR1-WR5 on an IFC2X3 IfcAxis2Placement3D), and its type_name is the violating entity's own type (an IfcExtrudedAreaSolidTapered was reported as IFCEXTRUDEDAREASOLID).
    • Rules declared on a supertype now bind subtypes the name matching missed: First/SecondOperandClosed read Closed on an IFC4X1-on IfcTriangulatedIrregularNetwork, and ApplicableMappedRepr judges the release's whole IfcShapeModel family.
    • Dim follows each release's IfcCurveDim and IFC4X3's IfcSegmentDim: the IFC4X1-on and IFC4X3 curve families (IfcOffsetCurveByDistances, IfcGradientCurve, IfcPolynomialCurve, IfcSpiral, ...) and an IfcCurveSegment's ParentCurve (slot 4) resolve, so the dimensional rules reach IFC4X3 alignment geometry, and IfcGridAxis.WR1 derives AxisCurve.Dim the same way. In IfcGetBasisSurface, an IFC4X3 IfcCurveSegment contributes its ParentCurve's surface, where slot 2 was read and an IfcCompositeCurveOnSurface of curve segments was reported as SameSurface.
    • A build that leaves the declared release's table out answers subtype tests from the compiled geometry chains and does not report a rule that demands a type for an entity those chains cannot classify.

openbim-ifc ​

Changed (breaking) ​

  • Behind geometry, the re-exported ifc_geometry is 0.9 (was 0.8): IFC4X3 stations, linear placements, offset curves by distances and sectioned solids/surfaces lower exactly (#307), and an IfcParameterValue distance on an alignment centreline is refused instead of being read as plan distance (#347).

Added (#330, checked authoring) ​

  • Feature authoring: ifc-author without choosing a release (author now means authoring plus schema, as before), and the checked authoring batch. apply_authoring(model, schema, ops, seed) runs AuthorOps in order against the model as the operations before them leave it and commits them as one transaction, or refuses with AuthoringError (the operation's position and an AuthoringFailure) and changes nothing; stage_authoring plans without committing.
  • AuthorOp (#[non_exhaustive]): Create an entity by type and named attributes, Edit named attributes, Remove an entity with the relationships that reference it, and the builders Project, Spatial (aggregated under its parent), Product (placed, contained and typed), TypeObject, AssignType, Contain, Aggregate, Placement (an IfcLocalPlacement over an IfcAxis2Placement3D) and OwnerHistory (through ifc-author's writers). Every record is built by name through ifc-author's EntityBuilder against the declared release, then its references are resolved (type checked with Schema::accepts_type) and its aggregates held to their declared bounds; a supplied GlobalId another entity holds is refused; an object is contained, aggregated or typed once, and a model holds one IfcProject.
  • An IfcRoot created without a GlobalId gets a name-based one over a seed (fresh_seed(), from the OS on native targets) and its id; a fixed seed reproduces a file. OwnerHistory is never invented.
  • authoring_handle(i) / HANDLE_BASE (2^62): an operation names the entity an earlier one produced; AuthoringOutcome::ids reports the ids.
  • Additive: a patch release. The property edit's name-based GlobalId moved into a shared module unchanged (a test pins its output).

[0.14.0] - 2026-10-09 ​

ifc-geometry ​

Semver: IfcPolygonalBoundedHalfSpace boundaries this crate refused as Unsupported now lower, and a malformed composite or indexed boundary is refused as Degenerate (an empty composite, a gap, an open curve, a point off the plane) instead of Unsupported. No public item is added or removed, and PARTIAL gains two rows; behaviour changes as it did for #335, so the next release is a minor one (0.14.0).

Added ​

  • IfcCompositeCurve and IfcIndexedPolyCurve as IfcPolygonalBoundedHalfSpace.PolygonalBoundary (#393). BoundaryType admits IfcCompositeCurve in IFC2X3, IFC4 ADD2 TC1 and IFC4X3 ADD2 and IfcIndexedPolyCurve in IFC4X3 ADD2; an IFC4 file using the latter lowers the same way, the curve being defined identically there, and the rule is left to validation. Both are read by the profile boundary readers (#43's composite walk, #335's indexed reading) under a half-space role, so SameSense, nested composites, trimmed IfcLine segments, the collinear-arc fallback ("treated as a polyline segment") and the closure rules are shared, and the closed, straight-edged result becomes the one Curve2::Polyline with closed set that SolidOperation::BoundedHalfSpace takes. Composite joints are judged within the model's Precision ("the tolerance under which two given points are still assumed to be identical", 1.E-5 project units when none is declared), and a point keeps the IfcPolyline boundary's rule: 2D, or 3D with z == 0 exactly, in Position's XY plane (BoundaryDim). Refused, naming the entity: as Degenerate, a gap between segments, an open curve, an empty composite, a point off the plane and the indexed refusals of #335; as Unsupported, a circular arc (a trimmed IfcCircle or a non-collinear IfcArcIndex), because Axiolid's BoundedHalfSpace takes only a Polyline2 (axiolid-mesh-compile 0.3.14 refuses every other Curve2, and Curve2 has no composite variant) and an arc is never polygonised; an IFC4X3 IfcCurveSegment member; and other segment parents. The IfcPolyline boundary is unchanged. PARTIAL lists the family's admitted and refused boundary forms. The fixture halfspace_boundaries_ifc4x3.ifc clips a wall by each form beside its IfcPolyline twin; each lowers to the twin's polyline and compiles with compile-reference-backend to its volume.

openbim-ifc ​

Changed (breaking) ​

  • Behind property-catalog, the re-exported property_catalog is ifc-template-catalog 0.4 (was 0.3): one compact snapshot container replaces the per-edition bincode files, with new snapshot and runtime modules (#317). The new property-catalog-runtime feature checks property writes against a catalog supplied at run time (#318).

Added (#326, attributes by name) ​

  • attribute_slots(schema, type_name) and attribute_slot(schema, type_name, name) (feature schema-api): an entity type's explicit attribute slots in Part 21 order, inherited first, as AttributeSlot (index, name, declared type, optional, aggregate, derived, declared_by). INVERSE and new DERIVE attributes hold no slot and are not listed; an inherited attribute a subtype redeclares as derived is marked derived. Names match ASCII case-insensitively, as EXPRESS identifiers do; answers use the schema's spelling.
  • attribute_by_name(model, schema, id, name) and set_attribute_by_name(model, schema, id, name, value): read and write by name against the schema the caller passes. The write refuses a derived slot and checks everything before its one write. NamedAttributeError (#[non_exhaustive]): MissingEntity, UnknownEntity, UnknownAttribute, DerivedAttribute.
  • Additive: a patch release.

Added (#318, runtime PSD/QTO catalog) ​

  • Feature property-catalog-runtime: property edits check Pset_/Qto_ sets against a catalog the host installs at runtime (property_catalog::runtime::install) instead of the embedded one; until the release's edition is installed such an edit is refused with the new PropertyEditFailure::CatalogNotLoaded { set, edition } and nothing is written. With property-catalog too, the embedded catalog is used. property-catalog now enables ifc-template-catalog/embedded explicitly. The embedded catalog shrinks from 3.7 MB to 1.4 MB with the catalog's compact format (#317).
  • Additive (PropertyEditFailure is #[non_exhaustive]): a patch release on its own.

[0.13.0] - 2026-10-09 ​

ifc-geometry ​

Semver: no public item is added, removed or changed in signature, and the net geometry is the same up to floating-point rounding. But the neutral graph lower_product_net/lower_product_net_with return is shaped differently, and the module documentation described that shape: a caller that matches NetLowering::root or a Subtraction::result as a Boolean, or reads a Subtraction::body as the opening's placed solid, now finds an Instance above it. Hosts the kernel refused also mesh now. That is an observable behaviour change for code that inspects the graph, so the next release is a minor one (0.13.0), not a patch.

Changed ​

  • Net lowering subtracts openings in the host's frame, not in world coordinates (#388). The host's Body is lowered without its world placement, each opening's Body by its placement relative to the host's, the Difference nodes run in that frame, and one Instance with the host's world transform (context WorldCoordinateSystem included, applied once) is put above NetLowering::gross, each Subtraction::body and each Subtraction::result; all of them stay in world coordinates. A host of several solids has each part cut in that frame. The relative placement is composed along the IfcLocalPlacement chain the opening and the host share, from their first common placement down, so a georeferenced site's translation and turn never enter it; a grid or linear placement may be that common placement or sit above it. When an opening shares no chain with its host (no PlacementRelTo path to a common placement, or a Body context whose frame differs from the host's), the net body is lowered in world coordinates exactly as before, rather than guessed.

Fixed ​

  • A wall with openings flush with both its faces, under a site placed at survey coordinates (600 000, 5 600 000) and turned to grid north, meshes net with compile-reference-backend (#388). In world coordinates the opening's faces were rounded off the wall's by up to an ulp of 5.6e6, and axiolid-mesh-compile 0.3.14 refused the result as touching itself along the opening's edges. The wall of ifclite-geometry/issue_098_wall_W.ifc, refused that way before, nets to 32.419 m³ now. Net volumes over the rest of the fixtures and the reference corpus are unchanged to 1e-8 m³.

openbim-ifc ​

Added (#123, property sets: write side) ​

  • apply_property_edits and stage_property_edits (feature properties): a batch of PropertyEdits (set or remove a value, addressed by object, set name and property name) planned against the model and staged on one Transaction, committed all or nothing. An inherited value is overridden on the occurrence, never on the type's shared set; a shared set or property entity is copied before it changes; an emptied set is removed with its relationship. Values are checked against the declared release (IFC2X3, IFC4, IFC4X3) and, with property-catalog, a Pset_/Qto_ set against the release's PSD/QTO catalog; without it such a set is refused (PropertyEditFailure::CatalogUnavailable). New sets and relationships take the object's owner history and a name-based GlobalId. Types PropertyEdit, SetType, PropertyEditOutcome, StagedPropertyEdits, PropertyEditError, PropertyEditFailure.
  • Additive: on its own a patch release; this release is a minor one for #306 anyway.

Changed (breaking) ​

  • validate, spatial, geometry-select, properties, classification, material, systems, cost and georef link only the releases the build names (#306): each release feature (ifc2x3 ... ifc4x3, and schema for all five) now forwards to their crates, which no longer pull every release's table in through ifc-schema's defaults. An IFC4-only build with all of them carries the IFC4 table alone. A build that enabled one of these features without a release feature must now add one (or schema): spatial and properties fail to compile without one, and the others refuse every file with their unsupported- or unbundled-schema error. full and domains (which implies schema through author) are unchanged. Under 0.x, a minor release.

Added ​

  • root_identity(model, schema, id) and RootIdentity (behind schema-api): the GlobalId and Name of an entity the given schema declares an IfcRoot, read by attribute name, never guessed from a slot (#123).
  • georeferencing(model) and GeoreferencingError (behind georef + properties): every IfcCoordinateOperation resolved with the project length unit, which ifc-properties resolves exactly; the join the language bindings carry (#123).
  • Re-exports Budget from ifc-model, which bounded domain traversals (a classification hierarchy) take.

[0.12.0] - 2026-10-08 ​

ifc-geometry ​

Semver: profile boundaries this crate refused as Unsupported now lower, a malformed IfcIndexedPolyCurve boundary is refused as Degenerate instead of Unsupported, and profile_outline refuses open indexed curves it used to close. No public item is added or removed, but behaviour changes, so the next release is a minor one (0.12.0).

Added ​

  • IfcIndexedPolyCurve as the OuterCurve and InnerCurves of IfcArbitraryClosedProfileDef and IfcArbitraryProfileDefWithVoids (#335), per IFC4 ADD2 TC1 and IFC4X3 ADD2: an IfcLineIndex becomes one straight Line2 edge per consecutive index pair, an IfcArcIndex the exact Circle2 through its three points with an angle domain, and a curve without Segments one straight edge per consecutive point pair. Nothing is chorded. The contour is the one the IfcCompositeCurve reader builds for the same outline (a trimmed IfcCircle for the arc), so both lower to the same Profile. An IfcArcIndex whose three distinct points are collinear lowers as the polyline start -> mid -> end (one edge when the middle point lies between the others, two when not), as the schema says: "the arc segment shall be treated as a polyline segment". Coincidence and collinearity are judged within the model's declared Precision (1.E-5 project units when none is declared), "after taking the Precision factor into account". Refused as GeometryError::Degenerate, naming the curve: an open curve (closure by index with Segments, by first and last point coinciding within the Precision without), segments that break WHERE rule Consecutive, an arc with two coincident points, SelfIntersect TRUE, and a 3D point list. Other boundary curve families are still Unsupported.

Changed ​

  • profile_outline refuses an open IfcIndexedPolyCurve boundary as GeometryError::Degenerate, as the profile lowering does (#335). It closed one implicitly before, reporting a ring whose closing edge the file never authored. Closure is the schema's: by index with Segments, by the first and last point coinciding within the model's Precision without. IfcPolyline rings are unchanged.

Fixed ​

  • IfcCurveBoundedPlane boundaries given as an IfcCompositeCurve of polyline segments compile with compile-reference-backend (#336). They lower unchanged, as a CurveRelation::Composite whose segments keep their SameSense; axiolid-mesh-compile 0.3.12 refused that relation ("is not a curve node"), and 0.3.13, the floor this crate already requires, resolves Composite and Trimmed boundaries (axiolid/kernel#255). No merge into one polyline is done here, so the segment structure stays in the neutral graph. A test now pins it: three segments with one reversed around a two-segment hole, and one reversed segment wrapping a clockwise ring, compile to their exact areas.

openbim-ifc ​

Changed (breaking) ​

  • Behind geometry, the re-exported ifc_geometry is 0.7 (was 0.6). Its select::subtype carries the IFC4X3 ADD2 supertype chains (#293), so is_a, select membership and BodyKind::classify now answer for IFC4X3-only entity names that previously classified as nothing; the new is_a_in/supertypes_of_in answer for one named release.

[0.11.0] - 2026-10-08 ​

ifc-geometry ​

Placement resolution covers every IfcObjectPlacement kind: linear placements compose with their alignment's frame (#357), grid placements resolve (#362), and any placement may be relative to a linear or grid one (#363). Error variants are added and resolved positions change for off-identity alignments and grid-placed products, so the next release is a minor one (0.11.0).

Added ​

  • IfcGridPlacement resolution (#362), in product_world_transform, products_world_transforms, PlacementResolver::world_transform and product lowering. Per IFC4.3 ADD2: the location is the intersection of the two axes' offset curves, OffsetDistances[1..2] to the LEFT of each axis ("anti-clockwise rotation through 90 degrees from the tangent"), reverted by IfcGridAxis.SameSense, and OffsetDistances[3] along the grid's Z; the x-axis is the first axis's tangent, an IfcDirection's x and y ratios, or the direction towards a second IfcVirtualGridIntersection; z is the grid's Z; all of it in the frame of the IfcGrid that lists the axes, whose ObjectPlacement composes above. Straight axes (IfcLine, a collinear IfcPolyline, an IfcTrimmedCurve on a line, an IfcOffsetCurve2D of one) intersect in closed form. Curved axes (IfcCircle, IfcEllipse, a bent IfcPolyline, an IfcTrimmedCurve on a conic with parameter trims, an IfcOffsetCurve2D of one) intersect through the caller's CurveEvaluator (LoweringSession::with_curve_evaluator, product_world_transform_with_evaluator, feature compile); without one they are refused as Unsupported naming the axis curve. Both the IFC2X3/IFC4 layout of IfcGridPlacement and IFC4X3's, which prepends the inherited PlacementRelTo, are read; GridPlacement::placement_rel_to is new.
  • GeometryError::GridAxesParallel { intersection, axes }, GridAxesDoNotIntersect { intersection, axes, detail } (curved axes that miss within their extent or meet more than once), GridAxisWithoutGrid { axis }, GridAxesInDifferentGrids { intersection, grids }, and PlacementRelToConflict { placement, stated, implied }: a stated PlacementRelTo that resolves to a different frame than the alignment's or grid's ObjectPlacement IFC4.3 says it references. A grid without ObjectPlacement is MissingAttribute.

Changed ​

  • An IfcLinearPlacement is placed in the frame its basis curve is stated in (#357), through both the cached CartesianPosition and the derived path: its PlacementRelTo when stated, otherwise the ObjectPlacement of the IfcAlignment (or other product) whose representation carries the basis curve. IFC4.3 ADD2 concept Product Linear Placement: "each product placement that uses Product Linear Placement references the IfcObjectPlacement of the IfcLinearPositioningElement through IfcLinearPlacement.PlacementRelTo"; IfcObjectPlacement.PlacementRelTo: "If it is omitted, then in the case of linear placement it is established by the origin of horizontal alignment of the referenced IfcAlignment Axis". CartesianPosition, a fallback for RelativePlacement, is read relative to the same frame. Products on an alignment placed off identity used to be placed as if the alignment sat at the origin, and CachedPositionPolicy::Verify refused their correct caches; both are fixed. CachedPlacementMismatch reports world positions. The context's WorldCoordinateSystem is still applied once, by the representation frame. derive_linear_placement_transform returns the frame in the basis curve's coordinates, as before; its documentation now says so.
  • PlacementResolver::world_transform walks PlacementRelTo through local, grid and linear placements alike (#363), caching each and keeping the cycle and depth refusals across mixed chains. A local placement relative to a linear placement used to be WrongEntityType, and one relative to a grid placement Unsupported. An entity that is no IfcObjectPlacement reports expected: "IfcObjectPlacement".

Added (#328, geometry in the bindings) ​

  • compile::Tolerance and compile::TriMesh re-export the tolerance every compile entry point takes and the mesh it returns, so a caller that names no axiolid-* crate (the openbim-ifc facade's mesh feature) can call them. Behind compile, as before.

Semver: additive; it ships with the minor release above (0.11.0), which the openbim-ifc facade's mesh feature needs.

openbim-ifc ​

Changed (breaking) ​

  • Behind step, the re-exported ParseOptions and OnMalformed are openbim-step 0.11's (were 0.8's), through ifc-step (#288). 0.11 adds ParseOptions::accept_real_without_point, included in ParseOptions::lenient(): a lenient read keeps a REAL written without its decimal point (1E-05) with a diagnostic instead of skipping its record. Strict reads still refuse it.

[0.10.0] - 2026-10-04 ​

ifc-geometry ​

Product lowering gains two opt-ins: Reference View openings taken as applied (#351) and an injected curve evaluator for linear placements (#353), which also checks a cached position (#354). The derived linear-placement frame now follows IFC4.3 (#355), which changes derive_placement_transform's output. API is added and behaviour changes, so the next release is a minor one (0.10.0).

Added ​

  • Net lowering of hosts whose IFC4 Reference View openings carry no Body (#351). lower::lower_product_net_with(session, product, NetOptions), with NetOptions::default().with_reference_only_openings( ReferenceOnlyOpenings::TakeAsApplied), lists an IfcOpeningElement whose representations are all Reference in the new NetLowering::taken_as_applied (TakenAsApplied { opening, relation, reason: AppliedReason::ReferenceRepresentationOnly }) and subtracts nothing for it; a host whose openings are all taken as applied lowers to its gross Body. IFC4 ADD2 TC1 IfcOpeningElement: a 'Reference' representation "is not subtracted, it is provided in addition to the hole in the Body shape representation of the voided element", and its Reference View concept says it "shall not be used to subtract the opening". lower_product_net is unchanged and still refuses such a host (ADR 0014, amended). An opening with no representation, one with another representation beside Reference, a Reference-only IfcVoidingFeature and an opening whose Body does not lower are refused whatever the option.
  • Evaluator-taking product lowering (#353), feature compile: LoweringSession::with_curve_evaluator(&dyn CurveEvaluator) places an IfcLinearPlacement without a cached CartesianPosition by deriving it from its RelativePlacement through the caller's evaluator, for every entry point that takes the session (lower_product_items, lower_product_representation, lower_product_net and its openings). The same for callers without a session: product_world_transform_with_evaluator, product_representation_frame_with_evaluator, and constraint::placement::derive::derive_linear_placement_transform for one placement. An IfcParameterValue on an alignment centreline is refused by name (#347). Without an evaluator nothing changes: the cache is read, and a placement without one is refused.
  • Cached-position check (#354), feature compile: with an evaluator, a cached CartesianPosition is compared with the derived location under CachedPositionPolicy::Verify (the default). Farther apart than the model's tolerance is the new GeometryError::CachedPlacementMismatch { placement, cached, derived, distance, tolerance }; within it, the derived frame is used, since IFC4.3 makes the cache "an optional fallback" for the linear expression. Only the location is compared. CachedPositionPolicy::Trust (LoweringSession:: with_cached_position_policy) uses a cache as it is and derives only uncached placements, for a model whose expressions this bridge cannot derive. The tolerance, derive::cached_position_tolerance, is the coarsest Precision of the model's 3D contexts, which IFC defines as "the tolerance under which two given points are still assumed to be identical", in the project length unit and converted to metres; IFC's default of 1e-5 project units when none is declared; floored at floating-point rounding (1e-9 relative, as the alignment seams).
  • LoweringSession::derives_linear_placements.

Changed ​

  • derive_placement_transform builds the IFC4.3 frame from the evaluator's point and tangent (#355): axes (tangent, left, up), where left is the horizontal Z x tangent and up is perpendicular to the tangent in its vertical plane. A positive OffsetLateral now moves LEFT, as IfcPointByDistanceExpression states; it moved right, and the product's local Z lay along the lateral. derive_linear_placement_transform also composes an explicit IfcAxis2PlacementLinear.Axis/RefDirection in that frame (8.9.3.4), which was ignored.
  • derive_placement_transform refuses a distance when the evaluator's distance_convention for the basis curve is not the one IFC states: plan distance on an IfcGradientCurve or IfcAlignment, arc length on a polyline. The reference evaluator agrees on all of them.
  • The refusal of an uncached IfcLinearPlacement names the evaluator-taking entry points.

openbim-ifc ​

Changed (breaking) ​

  • Behind ifcxml, the re-exported XmlCodec reads strictly by default when it has a schema: values are typed from the schema and undeclared names are refused (ifc-xml #266; see its changelog). .with_reading(SchemaReading::Lenient) restores the previous read.

Added ​

  • Behind ifcxml, re-exports SchemaReading and XmlLayout beside XmlCodec and XmlProfile, and with them the buildingSMART XSD layout reader XmlCodec::xsd (ifc-xml #265).

[0.9.0] - 2026-10-20 ​

ifc-alignment ​

Fixed ​

  • The writers' GlobalId check is ifc_model::guid::Guid::parse (#492): 22 characters were counted, so a text outside the IFC base-64 alphabet or with a leading digit above 3 was staged. Both are now refused with AlignmentError::Invalid naming GlobalId, before anything is staged.
  • The authoring::layout docs no longer claim ifc-author owns an IfcRelNests writer; they say how to stage the nesting (#492).

Semver: minor. A writer now refuses GlobalIds it accepted; a well-formed GlobalId behaves as before.

ifc-geometry ​

Input this crate refused now lowers exactly onto the station relations of axiolid-model 0.3.5 (ADR 0082), and a parameter along an alignment that a newer evaluator answered is refused. Behaviour changes and API is added, so the next release is a minor one (0.9.0).

Added ​

  • IFC4X3 stations (#307), stored exactly, never evaluated:
    • IfcPointByDistanceExpression lowers to GeometryNode::CurveStation.
    • IfcAxis2PlacementLinear lowers to GeometryNode::OrientedCurveStation. Axis and RefDirection pass through as components in the curve's (tangent, left, up) frame, which is how IFC4.3 ADD2 8.9.3.4 reads them.
    • IfcOffsetCurveByDistances lowers to CurveRelation::OffsetByStations. Offsets that stop short of the ends continue unchanged to them, as 8.9.3.42.3 states, through added stations when the basis states its length.
    • IfcSectionedSolidHorizontal lowers to SolidOperation::SectionsAtStations, and IfcSectionedSurface to SurfaceRelation::OpenSectionsAtStations, with the IfcOpenCrossProfileDef.Tags as section tags (matched as sets, an open section forwards or reversed).
    • New entry points: lower::lower_point_by_distance_node and lower::lower_axis2_placement_linear_node (module lower::station).
    • The five families move from dispatch::PLANNED to IMPLEMENTED, with PARTIAL rows for their refusals. BodyKind::classify gives IfcSectionedSolidHorizontal SectionedSpine and the two station items Point.
  • The conventions match IFC4.3 ADD2 exactly, so nothing is converted:
    • DistanceAlong is plan distance on an IfcGradientCurve (its parameter is the BaseCurve's, 8.9.3.34.1) and arc length elsewhere.
    • OffsetLateral is positive to the left.
    • OffsetVertical is perpendicular to the tangent in its vertical plane (StationFrame::Section).
    • OffsetLongitudinal runs along the tangent.
  • Section profile axes follow the documented upstream fix, not the printed ADD2 sentences: profile Y = Axis, normal = RefDirection, profile X = Axis x RefDirection (the left lateral by default). The sources are buildingSMART/IFC4.x-IF#147, IFC4.x-development#1010 and #1151, PRs #1162 and #1163, and IfcOpenCrossProfileDef 8.15.3.15.1. 8.8.3.35.1 says profile X is RefDirection, and 8.8.3.37.1 says X is Directrix x Axis, to the right. #344 tracks the upstream change.

Refused by name ​

  • DistanceAlong as IfcParameterValue, and a distance before the start or beyond a stated length.
  • A station on a tangent discontinuity of its basis, and a run of sections or offsets across one. IFC lets the previous segment's tangent govern (8.9.3.48.3) and mitres sections (8.8.3.35.1). The neutral evaluators read the next segment's tangent and do not mitre. Seams are read from stored data: polyline corners, and elevation-law breaks where the grade jumps. A basis whose seams cannot be located is refused: a curve relation, or a B-spline with a knot of multiplicity at least its degree.
  • A frame that scales, mirrors or tilts the vertical. WR2 (parallel axes). 2D axis directions.
  • On an offset curve: offsets short of an unbounded or unstated end, a non-zero OffsetLongitudinal, and a member on another basis.
  • On the sections: the WHERE rules NoLongitudinalOffsets, NoOffsets, CorrespondingSectionPositions and SectionsSameType, positions off the directrix or out of order, mixed tagging, and branching breaklines.

Fixed ​

  • constraint::placement::derive::derive_placement_transform refuses an IfcParameterValue DistanceAlong on an IfcGradientCurve or IfcAlignment basis curve by name (GeometryError::Unsupported on the basis curve) and no longer hands it to the injected CurveEvaluator (#347). IFC4.3 ADD2 does not define that parameter: a gradient curve takes its BaseCurve's (8.9.3.34.1), a composite accumulates the parametric ranges of its parent curves (8.9.3.20.1), which are angles for a circle (8.9.3.18.1) and u = s / (A sqrt(pi)) for a clothoid (8.9.3.19.1), and IfcCurveSegment says no parametric space is yet defined for its parent curves (8.9.3.28.1). The station lowering above refuses it the same way. A parameter on an IfcPolyline or line-only IfcIndexedPolyCurve, where IFC counts one per segment (8.9.3.51), is unchanged.
  • Released 0.3.0 through 0.8.1 passed that parameter through as the evaluator's native parameter. With axiolid-evaluate up to 0.3.5 the reference evaluator refused it, so nothing was placed. With axiolid-evaluate 0.3.6 (released 2026-10-04), which any of those releases resolves to, it reads the parameter as plan distance (axiolid ADR 0082) and answers: a product placed by an IfcParameterValue along an alignment lands at that plan distance, a position IFC does not give it. Another injected evaluator received the same undefined value. Upgrade to get the refusal.

Changed ​

  • Cargo.lock takes axiolid-construct 0.3.15, axiolid-mesh-compile 0.3.14 and, dev-only, axiolid-evaluate 0.3.6 and axiolid-reference 0.3.7. The workspace minimums below stay: nothing here relies on the new versions.
  • The workspace requires axiolid-model 0.3.5. With compile-reference-backend it requires axiolid-mesh-compile 0.3.13 and axiolid-construct 0.3.14, which resolve and mesh the station relations. Dev-only: axiolid-evaluate 0.3.5 and axiolid-reference 0.3.6.
  • An IfcCurveSegment placed by an IfcAxis2PlacementLinear is still refused. Its station lowers now, but no neutral relation places a curve in a station's frame (#311). IFCSEGMENTEDREFERENCECURVE stays in PLANNED (#311).

ifc-properties ​

Added (#469) ​

  • Conversion between units. ConversionUnit is a unit reduced to its SI dimensional exponents and its factor into SI base units, kept as a mantissa and a power of ten: from an SI name and prefix (ConversionUnit::si), a named conversion-based unit (ConversionUnit::named: the IFC4 Table 673 designations inch, foot, yard, mile, square foot, acre, cubic foot, litre, gallon US, pound, degree, psi, ... with their exact legal factors where the table rounds, and US survey foot; listed by named_units), text (ConversionUnit::parse: MILLIMETRE, MILLI METRE, SQUARE_MILLIMETRE, foot), or a model's unit entity (resolve_unit: IfcSIUnit prefixes, IfcConversionBasedUnit factor chains and IfcDerivedUnit exponents through the exact resolver). unit_conversion checks the dimensions agree and gives a UnitConversion; prefixed SI units convert by an exact power of ten (millimetres to metres divides by 1000). convert_value and convert_between (two unit entities of a model) convert one value.
  • Refused with UnitConversionError: units of different dimensions, IfcMonetaryUnit, IfcContextDependentUnit, and IfcConversionBasedUnitWithOffset or degree Fahrenheit by name, whose offset is not applied until #110 is settled. The SI DEGREE_CELSIUS converts affinely.
  • format_length: a length as decimal text in any length unit, or as feet and inches (5' - 6 1/2") or inches rounded to a fraction of an inch (LengthFormat).
  • measure_unit_type: the unit type a measure takes in a release, as the exact resolver derives it (IfcNonNegativeLengthMeasure takes a LENGTHUNIT); exact_project_unit: the project's unit of a type.

Changed (#469) ​

  • The exact unit resolver also tracks each unit's factor as a mantissa and a power of ten. ExactUnit and every answer are unchanged.

Semver: minor (additions).

openbim-ifc ​

Added ​

  • tests/stationing_template.rs: a referent's Pset_Stationing authored by alignment and read by properties checks clean against the property-catalog IFC4X3 ADD2 corrected profile (#216).

Changed (breaking) ​

  • PanelPosition and Unreachable are #[non_exhaustive]: a match needs a wildcard arm.

  • Sector and ContainerElements are #[non_exhaustive]; they can no longer be built with a struct literal outside the crate.

  • Code previously behind schema is behind schema-api, which schema and every release feature imply; schema::for_version returns a Result (see ifc-schema).

Changed ​

  • The georeferencing and alignment conformance test gives its placeholder IfcPolyline two points (Points is LIST [2:?], checked since #111).
  • Door and window operation reads name the type-object entity per verified release (IFC2X3, IFC4, IFC4X3) and refuse any other with ExactPropertyError::UnsupportedSchema, instead of treating every non-IFC2X3 release as IFC4.

Added ​

  • Per-release schema features (#112): ifc2x3, ifc4, ifc4x1, ifc4x2, ifc4x3, each providing the schema API with that one bundled table, and schema-api (the API with no table). schema keeps its meaning: the API with every release. A single-release build refuses the others through schema::for_version with schema::NotBundled. Domain features link the releases their crates read, so enabling one brings every release it reads.
  • compiled_features() reports schema-api and each release feature.
  • tests/intermediate_releases.rs: IFC4X1 and IFC4X2 files resolve to their own SchemaVersion, release id and bundled table (#33).

[0.8.1] - 2026-10-10 ​

ifc-alignment ​

Semver: a patch release. No public item or behaviour changes; the dependency floors rise with the workspace (ifc-geometry #418, #414).

Changed ​

  • Requires axiolid-curve 0.3.6 (and axiolid-evaluate 0.3.10 for the tests), the releases that add the neutral CurvePath (axiolid/kernel#290) and measure stations along offsets (axiolid/kernel#289). Nothing here builds either; every test passes unchanged against them.

ifc-geometry ​

No public API changes. The reference backend's floor rises and input it refused now compiles, as with earlier kernel floors (0.3.1): the next release is a patch (0.8.1).

Changed ​

  • compile-reference-backend requires axiolid-mesh-compile 0.3.12 and axiolid-construct 0.3.13 (axiolid/kernel#245, #315). An IfcCurveSegment over a 2D IfcPolynomialCurve (the CUBIC transition, #90) lowers to a Bezier trimmed at TrimSelector::ArcLength. 0.3.4 read parameter selectors only and refused that trim by name; 0.3.12 resolves it by quadrature, so the segment compiles. A swept disk whose polyline directrix turns a sharp corner without a fillet radius compiles with an exact half-angle mitre, so its volume is the section area times the centreline length. 0.3.9 to 0.3.11 refused that corner, so a downstream build that resolved them fresh refused such pipes. The kernel still refuses three cases by name (axiolid/kernel#248): a sharp corner beside an arc, a closed polyline, and FilletRadius equal to Radius.

ifc-properties ​

Added ​

  • PropertyIndex: the exact resolver's view of one model, built once by PropertyIndex::build(&model) and asked per object (#352). It validates every IfcRelDefinesByProperties and IfcRelDefinesByType once and records each object's assignments, so resolving every object is linear in objects plus relationships instead of their product. Its exact_property, exact_properties, exact_properties_where, exact_property_sets_where and exact_predefined_sets answer exactly what the free functions of the same name answer, refusals included, in the same precedence. It borrows the model (ADR 0003), so the model cannot be edited while an index exists; rebuild it after a commit. Measured on a generated IFC4 model (benches/properties, benchmarks/baseline.md): one property for every one of 100,000 walls takes 2.0 s instead of about 31 hours (extrapolated; 10,000 walls: 195 ms instead of 814 s), scaling ten-fold per ten-fold more walls.

Changed ​

  • The free functions scan the relationships for the one queried object through the same code, so their answers are unchanged (a dump of every answer over the fixture corpus and the new equivalence models is byte-identical before and after); with ifc-schema's precomputed tables a single call is several times cheaper, but it still validates every relationship of the file, as it must.

Semver: one new public type, nothing changed or removed: a patch release.

openbim-ifc ​

Changed ​

  • Requires the patch releases published with it: ifc-properties 0.5.1 (bare quantity values, IFC2X3 *_with_owner_history authoring, type objects in the exact API), ifc-cost 0.2.2, ifc-classification 0.2.2 (IFC4X3 binding) and ifc-systems 0.2.2.
  • door_operation and window_operation given a type object (an IfcDoorType, IFC2X3 IfcDoorStyle, ...) refuse it with NotADoor or NotAWindow, where they returned Property(InvalidQueryObject): the exact property reader now accepts type objects (ifc-properties #193). Results and refusals for doors and windows are unchanged.

[0.8.0] - 2026-10-09 ​

ifc-alignment ​

Semver: a minor release (0.8.0). No public item changes, but lower_segmented_reference_curve and segmented_reference_curve3 now return a curve for input they refused, and its pivot law can hold a held-rail piece (CantForm::AboutRail), which only axiolid-curve 0.3.5 and axiolid-evaluate 0.3.9 or later read. Dependency floors rise to those releases.

Added ​

  • lower_segmented_reference_curve / segmented_reference_curve3 lower a VIENNESEBEND that rotates about a held rail (#364): when one rail has the same height at both ends (at the model's declared precision, as CantLayout compares it), the pivot law is Axiolid's held-rail piece, CantPiece::about_rail, on that rail at its authored height, and the rotation point held -+ b sin(psi) / 2 follows the bend's angle law (axiolid/kernel#279, Axiolid ADR 0081 amendment). The banked curve and CantLayout::frame_at_distance now agree inside such a bend: cant, bank angle and rotation point to rounding; the rail heads within the BankConvention::TangentRotation drift (D / 2)(1 - cos theta), the banked section's rails standing square to the tangent while the cant layout states them vertically (under 0.02 mm for 150 mm cant on a 2% grade, nothing on the level).

Changed ​

  • The rotation stays BankConvention::TangentRotation: IFC4.3 ADD2 states cant as the angle psi = arcsin(D / b) (IfcAlignmentCantSegmentTypeEnum, 8.7.2.1) and IfcSegmentedReferenceCurve (8.9.3.62) carries it by interpolating the placement axes, a rotation of the section frame. A held rail therefore drifts by (D / 2)(1 - cos theta) from its authored height on a grade, theta the climb of the rotation point's path; this is documented in curve/reference.rs and cant/frame.rs.
  • The Unsupported reason naming the missing angle-form pivot is gone. A Viennese bend whose pivot moves with neither rail held is still refused by both the lowering and CantLayout, with the same typed error.
  • Requires axiolid-curve 0.3.5 (the held-rail pivot), and for the tests axiolid-evaluate 0.3.9, which evaluates it.

ifc-geometry ​

Input this crate refused now lowers exactly, onto the Axiolid relations of axiolid-curve 0.3.3 and axiolid-model 0.3.4: behaviour changes, so the next release is a minor one (0.8.0).

Changed ​

  • An IfcCurveSegment over a 2D IfcPolynomialCurve (the CUBIC transition) lowers to its exact Bezier, placed rigidly and trimmed at TrimSelector::ArcLength(SegmentLength), instead of refusing (#90). A non-zero SegmentStart, a backwards walk, a 3D polynomial, and one with no degree-one coordinate to bound the trim stay refused by name.
  • An IfcGradientCurve whose base curve holds such a segment has a Curve2::Chain plan: the polynomial is a parametric piece read by arc length (#90).
  • A vertical IfcCircle segment in an IfcGradientCurve lowers to ElevationLaw::CircularArc, and a vertical IfcSpiral (the IfcClothoid) to ElevationLaw::Intrinsic, its extent read from the next segment's start (#258).
  • dispatch::PLANNED keeps IFCPOLYNOMIALCURVE (unbounded on its own; it lowers as an IfcCurveSegment parent) and IFCSEGMENTEDREFERENCECURVE, now citing #311: the roll law exists, but the geometric form states cant through stations (#307) and parent curves with no normative mapping. An IfcAxis2PlacementLinear placement now cites #307.

ifc-properties ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with property sets shrinks from 1,519,870 to 926,986 bytes. The default build links every release, as before.
  • A file declaring IFC2X3, IFC4 or IFC4X3 when the build leaves that release out is refused by the exact reads with UnsupportedSchema instead of panicking. Permissive reads (type objects in property_sets_by_object, template reads) keep the IFC4 table as their baseline and fall back to the newest bundled one in a build without it. A build naming no release fails to compile with a message naming the features, so a build without default features has to name one.

Under 0.x, a minor release.

openbim-ifc ​

Changed (breaking) ​

  • properties re-exports ifc-properties 0.5.0, whose breaking changes pass through (see that crate's changelog):
    • Quantity and QuantityKind are #[non_exhaustive], and gain Quantity::Unresolved for a quantity without a readable value (#138) and QuantityKind::Number for IFC4X3 IfcQuantityNumber;
    • create_quantity / create_quantity_with take the &Model they write into and write its release's layout;
    • PropertyTemplate gains fields and is #[non_exhaustive] (#108).
  • Requires ifc-spatial 0.2.3 (referenced_elements, release-bound container classification) and ifc-geometry 0.4.3 (BodyItem::item_world, #185).

Added ​

  • spatial_properties(model) (features spatial and properties): every spatial container in tree order, depth first from the roots, with the elements it holds, each with its exact_properties list, in IFC2X3, IFC4 and IFC4X3 (#121). An element is listed as Contained (IfcRelContainedInSpatialStructure), Referenced (IfcRelReferencedInSpatialStructure, so an element spanning several storeys appears under each) or Part (an IfcRelAggregates part, at any depth, of a contained element, which the Element Composition concept places by its composite's containment), ordered by element id. A nested space is its own container, not folded into its storey. A model-level refusal (diagnostics, missing or unsupported schema) is the function's error; any other ExactPropertyError is reported on the element it concerns and the other elements are still returned. Properties resolve lazily as ContainerElements::elements (or elements_where, with exact_properties_where selectors) is iterated. New types: SpatialProperties, ContainerElements, SpatialContainer, ContainerName, ElementMember, ElementProperties and SpatialMembership. Needs the next ifc-spatial release, which adds SpatialTree::referenced_elements.

Changed ​

  • The spatial feature classifies spatial containers from the file's declared release (#121, via the next ifc-spatial release, which now links ifc-schema): IFC4X3 facilities and facility parts such as IfcRoad, IfcRoadPart, IfcBridge and IfcBridgePart, and IfcExternalSpatialElement, are containers, so SpatialTree, spatial_properties and unreachable_products see the elements placed in them. Containment or reference into a non-container is reported as a SpatialAnomaly. unreachable_products skips containers by the tree's classification instead of a name test.

[0.7.3] - 2026-09-27 ​

openbim-ifc ​

Changed ​

  • The crate README, which is the crates.io page, is rewritten. The published one told readers to depend on a Git revision and said the crates were not on crates.io. The code is unchanged since 0.7.2.

[0.7.2] - 2026-10-09 ​

ifc-alignment ​

Semver: a patch release. No code, API or behaviour changes; only the dependency floor rises with the workspace (#346).

Changed ​

  • Requires axiolid-model 0.3.7 (and axiolid-evaluate 0.3.8 for the tests), the releases that measure stations along curve relations (axiolid/kernel#285) which ifc-geometry lowers onto. Nothing here reads a station; every test passes unchanged against them.

openbim-ifc ​

Added ​

  • window_operation(model, window) (features geometry-select and properties), the window counterpart of door_operation: each panel of a window as a world frame, width, height, hinge side, swing Sector for a side hinge and tilt Sector for a top or bottom hinge, from its placement, OverallWidth/OverallHeight, partitioning (IfcWindowType.PartitioningType, IFC2X3 IfcWindowStyle.OperationType, or the occurrence's), IfcWindowPanelProperties and the mullion and transom offsets of IfcWindowLiningProperties, in IFC2X3, IFC4 and IFC4X3 (#170). Single, double and triple partitionings with side-hung, tilt-and-turn, top- and bottom-hung, sliding, removable and fixed panels are derived; NOTDEFINED and USERDEFINED partitionings, pivot, OTHEROPERATION and NOTDEFINED panels, a window without panel properties or overall size, panels that contradict the partitioning, and a split without a valid lining offset are refused as WindowOperationError, never defaulted. Panels tile the placement's XZ plane; lining, mullion and transom thicknesses are not applied. Sector and Side are shared with door_operation, unchanged.

[0.7.1] - 2026-10-09 ​

ifc-alignment ​

Semver: a patch release. No code, API or behaviour changes; only the dependency floor rises with the workspace (#346).

Changed ​

  • Requires axiolid-model 0.3.6 and axiolid-curve 0.3.4 (and axiolid-evaluate 0.3.7 for the tests), the releases that add seam sides to stations (axiolid/kernel#263) which ifc-geometry lowers onto. Nothing here reads a station; every test passes unchanged against them.

openbim-ifc ​

Added ​

  • profile_outline and ProfileOutline are re-exported at the root under geometry-select (#166), beside describe_profile: the straight-edged outline of an arbitrary closed profile, without the geometry kernel.

Changed ​

  • Requires ifc-geometry 0.4.1, which carries profile_outline, product_representation_frame and the curve-bounded plane fix (#163).

[0.7.0] - 2026-10-20 ​

ifc-alignment ​

Behaviour fixes with no API change, but a patch release is not enough: input that evaluated before is now refused (a Viennese bend whose rotation point moves with neither rail held), and the banked lowering's refusal reasons change, so the next release is a minor one (0.7.0).

Added ​

  • cant_at, CantLayout::cant_at_distance and so CantLayout::frame_at_distance evaluate a VIENNESEBEND that rotates about a held rail (#364): when one rail has the same height at both ends (compared at the precision the rotation points are compared at), that rail stays and the other is the held rail +- D(xi), with D = b sin(psi(xi)) from the bend's angle law. Rotation about the low rail, the usual case in rail practice, now evaluates inside the bend: from D = 0 to 0.15 m on b = 1.5 m about the right rail, the left rail mid-bend is 0.0750942 m.

Fixed ​

  • cant_at, CantLayout::cant_at_distance and so CantLayout::frame_at_distance evaluate a VIENNESEBEND for the section's cant D = left - right, as IFC4.3 ADD2 writes it (IfcAlignmentCantSegmentTypeEnum, 8.7.2.1): psi = arcsin(D / b), psi(xi) = psi1 + dpsi xi^4 (35 - 84 xi + 70 xi^2 - 20 xi^3), D = b sin(psi), with the rails e +- D / 2 about a rotation point e = (left + right) / 2 that stays put (#312). They no longer blend each rail's height as if it were a cant: rotating about the centreline from D = 0 to 0.15 m on b = 1.5 m, the cant mid-bend is now 0.0750942 m, not 0.0750235 m. Inside a bend CantFrame now agrees with the banked centreline (Curve3::Banked) and with the horizontal Viennese-bend law, which already used D.

Changed ​

  • Inside a VIENNESEBEND whose rotation point moves with neither rail held, cant evaluation now returns AlignmentError::Unsupported, as the banked lowering does: the standard states the bend for the section's cant angle only, so these rails are not determined. The authored start and end values still evaluate, so the horizontal Viennese-bend law, which reads the cant at its own ends, is unaffected where those meet the cant bend's ends. CantLayout compares the two rotation points at the model's declared precision, as the banked lowering does, so CantLayout::resolve of a layout holding a Viennese bend now also refuses a malformed IfcGeometricRepresentationContext.Precision. cant_at alone compares them to floating-point rounding.
  • A Viennese bend's |D| > b check applies to the cant D, not to each rail's height.
  • lower_segmented_reference_curve / segmented_reference_curve3 refuse the same Viennese bends as before, now with two distinct AlignmentError::Unsupported reasons: a held rail (the low rail, say), whose rotation point held -+ b sin(psi) / 2 is an angle form that Axiolid's height-form pivot law cannot carry (BankError::AngleInPivot) until axiolid-curve has an angle-form pivot (#364); and a pivot that moves with neither rail held, as cant_at gives. For a held-rail Viennese bend the section frame therefore has a value where the banked curve refuses; everywhere else they agree.

ifc-geometry ​

select::subtype carries IFC4X3 ADD2 supertype chains (#293). Answers change for IFC4X3-only entity names, and a build without default features links fewer schema tables (#306), so the next release is a minor one.

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table. The default build links every release, as before, but a build without default features (the kernel-free column, openbim-ifc's geometry-select) now links only the releases it names; release-bound authoring refuses the others with GeometryError::AuthoringSchemaUnbound. Representation selection reads no table and is unaffected. lowering still links every release through ifc-alignment.

Added ​

  • select::is_a_in, supertypes_of_in and known_entities_in answer for one named release, and VERIFIED_SCHEMA_VERSIONS lists the releases they answer verbatim: IFC4 ADD2 TC1 and IFC4X3 ADD2. tables_are_verified_for(Ifc4x3) is now true.
  • The IFC4X3 rows are a delta over the IFC4 tables: the 19 concrete geometry, profile and placement entities IFC4X3 adds, the abstract supertypes they introduce (IfcSpiral, IfcSegment, IfcOffsetCurve, IfcDirectrixCurveSweptAreaSolid, IfcSectionedSolid), and the IFC4X3 chains of the four IFC4 entities IFC4X3 re-parents. tests/schema_coverage.rs compares every chain, per release, with IFC4.exp and IFC4X3_ADD2.exp.

Changed ​

  • The release-neutral is_a and supertypes_of now resolve IFC4X3-only entities with their IFC4X3 chain: is_a("IFCCLOTHOID", "IFCCURVE"), is_a("IFCGRADIENTCURVE", "IFCCOMPOSITECURVE"), is_a("IFCTRIANGULATEDIRREGULARNETWORK", "IFCTESSELLATEDFACESET") and is_a("IFCOPENCROSSPROFILEDEF", "IFCPROFILEDEF") were false and are true. Every answer for an entity IFC4 declares is unchanged, including the four IFC4X3 re-parents (IfcOffsetCurve2D, IfcOffsetCurve3D, IfcFixedReferenceSweptAreaSolid, IfcSurfaceCurveSweptAreaSolid), which keep their IFC4 chain unless a caller asks is_a_in(Ifc4x3, ..).
  • Visible effects: the select resolvers (GeometricSetSelect, BooleanOperand, PointOrVertexPoint, ...) accept IFC4X3-only members instead of reporting a wrong type. BodyKind::classify returns Curve for the six IfcSpiral subtypes, IfcPolynomialCurve, IfcOffsetCurveByDistances and IfcSegmentedReferenceCurve, and Surface for IfcSectionedSurface, so body description lists those items instead of refusing them. Lowering is unchanged: those families still lower, or refuse with the dispatch::PLANNED reason, as before. Where-rules that test inheritance (dimensionality, swept-area rules) now also apply to the IFC4X3 subtypes the schema makes them inherit.

ifc-georef ​

Added ​

  • IFC2X3 georeferencing through the ePSet_MapConversion and ePSet_ProjectedCRS property sets (#451): resolve_property_set_georeferencing resolves them into the same ProjectToMap as IFC4's IfcMapConversion and IfcProjectedCRS, with the project and map length units resolved, and has_property_set_georeferencing tests for them cheaply. The source is the buildingSMART Australasia "User Guide for Geo-referencing in IFC" v2.0 (2020), §5.1 Tables 2 to 4: the property names and measure types, and the sets on IfcProject. Where the guide is silent the reader follows established practice, documented as such: set names match ignoring case (ePSet_, ePset_, EPset_), IfcSite is a legacy host read only when the project carries no set, and an optional MapUnit text names an SI length unit. Precedence: the declared release decides, IFC2X3 through the sets and IFC4/IFC4X3 through the entities, never merged.
  • OperationSource::PropertySet (the host object), and the MAP_CONVERSION_PSET and PROJECTED_CRS_PSET names.
  • Typed refusals for a malformed or incomplete set: GeorefError::IncompletePropertySets (one set of the pair, never joined across hosts), AmbiguousPropertySets (two sets of one name on a host), MissingProperty (a required property without a value) and InvalidProperty (not a single value of the guide's type, not finite, or a length whose own unit is not the map unit). A repeated property name is RuleViolation WR32.
  • Fixtures test/fixtures/synthetic-georef/ from tools/gen_georef_fixtures.py: an IFC2X3 file checked against its IFC4 twin, the site fallback, and refusal cases.

Semver: minor. Additions only; the new enum variants are on #[non_exhaustive] enums.

ifc-properties ​

Added ​

  • create_monetary_unit(tx, model, draft) stages an IfcMonetaryUnit in the model's declared release (#232). Currency is an IfcCurrencyEnum in IFC2X3 TC1 and an IfcLabel from IFC4 on: IFC2X3 writes the enumerator token (.EUR., matched ignoring case) and refuses a currency the enumeration does not list with PropertyError::AuthoringInvalid; IFC4 and IFC4X3 write the text given. A header binding no single supported release is refused with MultipleSchemas or UnsupportedSchema, as for the other release-bound writers.

  • PropertyError::ValueForm { entity, attribute, declared, typed_required, found } (#215): a value of the declared type written in the form ISO 10303-21 does not use for it, mirroring ifc-author's AuthorError::ValueForm. PropertyError is #[non_exhaustive], so the new variant is additive.

Changed ​

  • Behaviour change: every writer of an IfcValue slot refuses a bare literal before staging (#215). IfcValue is a SELECT in every release, whose members are all defined types, so its value is written as a typed parameter (IFCLENGTHMEASURE(2.5), §12.1.8); a bare 2.5 was accepted and then reported by ifc-validate as type.select.untyped. Covered: add_property_single_value (NominalValue), add_property_enumerated_value and add_property_enumeration (EnumerationValues), add_property_bounded_value (each bound and the set point), add_property_list_value (ListValues), add_property_table_value (DefiningValues, DefinedValues) and add_measure_with_unit (ValueComponent). A bare number, string, boolean, logical or binary is PropertyError::ValueForm with typed_required: true; a wrapper around a wrapper is ValueForm with typed_required: false; a reference, aggregate, enumeration constant, *, or a wrapper around $ is PropertyError::AuthoringInvalid. Callers passing Value::Real(..) and the like must wrap the value in the measure it is (Value::Typed { type_name: "IFCLENGTHMEASURE", .. }). Which member a wrapper names is not checked here: these writers take no model, and ifc-validate judges it against the declared release.

Deprecated ​

  • add_monetary_unit: it takes no model and always writes IFC4 text, which is schema-invalid in IFC2X3. Use create_monetary_unit (#232).

Fixed ​

  • unit() and project_units() read an IFC2X3 IfcMonetaryUnit whose Currency is an IfcCurrencyEnum token: UnitKind::Monetary now carries the currency instead of None, and the quantity unit check that consumes it sees the currency too (#232).

ifc-validate ​

Added (#461) ​

  • express: an EXPRESS rule evaluator (feature express, default). ExpressRules::parse reads a release's .exp text, which the caller supplies (the schemas are CC BY-ND 4.0; nothing of their rule text is bundled or generated), checks it against the bundled tables (same schema, same slots for every entity) and resolves every name. validate_with_options with ValidateOptions::express then evaluates every entity WHERE rule (inherited ones on every subtype), every type WHERE rule (wherever a value of the type appears: directly, in an aggregate, through a defined type or wrapped in a SELECT) and every global rule, with the FUNCTIONs they call, derived attributes and INVERSE attributes, under ISO 10303-11 three-valued logic. Every declaration of IFC2X3 TC1, IFC4 ADD2 TC1, IFC4X1, IFC4X2 and IFC4X3 ADD2 is evaluable (ExpressRules::coverage; data/express-coverage.tsv): 339, 652, 658, 672 and 752 entity rules. A rule evaluating FALSE is an error under its Entity.Label id (Type.Label, global.Rule); UNKNOWN or ? is the new Severity::Indeterminate; a construct the evaluator lacks is reported once as unsupported, by name; data a rule cannot read is an evaluation error. EvalLimits bounds steps per rule, nesting and the whole run; each limit is a typed BudgetExceeded. Supplied rules replace the hand-written ones with the same id; on the corpus and on every hand-written rule's adversarial pair the two agree. On this repository's fixtures, IfcOpenShell's rule and validation fixtures and the IFC4 annex E examples, verdicts match IfcOpenShell's express_rules but for the differences docs/coverage.md lists.
  • structure::inverse_cardinality (structure.inverse.cardinality): every INVERSE clause's bounds, a single inverse meaning exactly one referrer. Off by default (ValidateOptions::inverse_cardinality), on with EXPRESS rules; it matches IfcOpenShell's findings on the corpus.
  • ValidateOptions and validate_with_options: budget, EXPRESS rules, INVERSE cardinality and a property-set catalog in one run.
  • Finding::detail (FindingDetail): an EXPRESS result's release, scope (RuleScope), label and RuleOutcome, or a ReservedSetName. Finding::indeterminate, Finding::with_detail, Summary::indeterminate.

Added (#511) ​

  • property.reserved_set_name: an IfcPropertySet or IfcElementQuantity whose name starts with Pset_ or Qto_ (any case) but which the release's PSD/QTO catalog does not define is a warning (bSI PSE001). The catalog is supplied through the PropertySetCatalog trait (ValidateOptions::catalog); this crate links none. Without one, such names are reported once as unsupported.

Changed ​

  • Summary prints its indeterminate count: 0 errors, 0 evaluation errors, 0 warnings, 0 indeterminate, 2 unsupported.
  • validate reports a file using Pset_/Qto_ names with one unsupported property.reserved_set_name finding when no catalog is supplied.
  • global.UniqueGlobalId runs under IFC4X1 and IFC4X2 too.

Semver: minor (0.7.0). Severity, Finding and Summary are #[non_exhaustive], so their new variant and fields break no caller; the new findings and the summary text change what a report holds.

Changed (#467) ​

Semver: minor. ValidateError is #[non_exhaustive] and gains a variant; a new finding id.

  • validate_declared refuses a file declaring IFC4X3_TC1 or IFC4X3_ADD1 with the new ValidateError::UnpublishedSchema(token) instead of UnknownSchema: a real IFC release, but one buildingSMART publishes no EXPRESS schema for, so no tables exist and it is not validated as IFC4X3 ADD2 by assumption.
  • The header check reports such a token as the warning header.schema.unpublished, naming the release (IFC 4.3.0.1 or 4.3.1.0) and carrying FindingDetail::UnpublishedSchema(release), instead of header.schema.unknown. A caller that chooses to validate such a file as ADD2 passes those tables to validate itself; the report then carries this warning, so it never reads as a check against the declared release.

openbim-ifc ​

Changed (breaking) ​

  • Re-exports three crates whose breaking releases pass through: validate is ifc-validate 0.3.0 (Severity::EvaluationError, renamed rule ids, Budget::max_depth removed), material is ifc-material 0.3.0 (views and authoring bound to the declared release; create_material takes the model), and geometry is ifc-geometry 0.4.0 (typed reference errors, IfcBlock meshes no longer offset by half their extents). See each crate's changelog.
  • Requires the releases published with it: ifc-properties 0.4.1, ifc-spatial 0.2.2, ifc-xml 0.2.1, ifc-alignment 0.3.1, ifc-author 0.2.1, ifc-control 0.2.1, ifc-cost 0.2.1, ifc-tabular 0.2.1 and ifc-template-catalog 0.2.1.

Added ​

  • body_description, describe_profile and their types (BodyDescription, BodyItem, BodyKind, SweptSolid, SweepPath, ProfileDescription, ProfileParameters) are re-exported at the root under geometry-select (#147), so a rule check reads a body's kind and swept-solid profile parameters without linking the geometry kernel.
  • door_operation(model, door) (features geometry-select and properties): each leaf of a door as a world frame, width, hinge side and swing Sector, from its placement, OperationType and IfcDoorPanelProperties, in IFC2X3, IFC4 and IFC4X3 (#148). Single and double swing, double-acting, sliding, rolling-up and swing-fixed doors are derived; NOTDEFINED, USERDEFINED, revolving, folding, lifting and the DOUBLE_DOOR_SINGLE_SWING_OPPOSITE_* operations, a door without panel properties or OverallWidth, and panels that contradict the operation are refused as DoorOperationError, never defaulted. The leaves lie on the placement's x axis; lining offsets across the wall depth are not applied.

Changed ​

  • The properties feature also names ifc-schema, which ifc-properties already links, so the door join reads attributes by name from the bound release's table. No crate is added to a build.

[0.6.1] - 2026-10-03 ​

ifc-geometry ​

IFC4X3 ADD2 geometry families (#243). Every concrete IFC4X3 representation item and profile is now lowered or refused with a named reason.

Added ​

  • IFC4X3 alignment curves lower exactly. IfcCurveSegment lowers for IfcLine, IfcCircle, 2D IfcPolyline and all six IfcSpiral parents, placed by its Placement and cut by its IfcLengthMeasure arc lengths: a polyline, an angle-trimmed circle, or a planar Curve3::Intrinsic carrying the spiral's curvature law rebased to the segment in closed form. A negative SegmentLength walks the parent backwards; the zero-length closing segment is its placement, and a composite drops it. IfcCompositeCurve now accepts IfcCurveSegment members.
  • The spiral laws read the IFC4.3 terms: sign(A_n) s^n / |A_n|^(n+1) for the clothoid and the second-, third- and seventh-order polynomial spirals; 1/A_0 + cos(pi s/L)/A_1 (cosine) and 1/A_0 + sign(A_1) s/A_1^2 + sin(2 pi s/L)/A_2 (sine), with L the using segment's length. A present zero term is refused, not read as absent.
  • IfcGradientCurve lowers to Curve3::Elevated: its BaseCurve as one intrinsic plan with a piecewise curvature law, its vertical segments (IfcLine grades, degree-2 IfcPolynomialCurve parabolas) as a piecewise elevation law. Seams are checked in closed form: headings everywhere, positions after a line or arc, parabola arc lengths.
  • IFC4X3 IfcOpenCrossProfileDef lowers exactly through lower_open_profile_node to an open polyline whose vertices are the closed-form sums of its widths and slopes (horizontal or along-slope widths, OffsetPoint honoured, slopes measured from +X towards +Y as the IFC4.3 figure states). describe_profile reads it as the new ProfileParameters::OpenCross, in metres and radians, refusing a Widths/Slopes or Tags count mismatch, a negative width and a vertical slope with horizontal widths. It bounds no area, so the area-profile path refuses it, as for IfcArbitraryOpenProfileDef. section_slot::OC_* are its slot constants.
  • IFC4X3 IfcDirectrixDerivedReferenceSweptAreaSolid lowers to the same exact FixedReferenceSweep as its supertype when the directrix defines only a tangent, which IFC4.3 says is the identical behaviour. A directrix that defines a tangent plane (built from IfcCurveSegments, such as IfcGradientCurve and IfcSegmentedReferenceCurve, or lying on a surface) is refused as Unsupported naming the missing neutral primitive, the public lower::swept::DIRECTRIX_DERIVED_TANGENT_PLANE.
  • IfcTriangulatedIrregularNetwork (IFC4X3) lowers as a triangle mesh when every Flags value is a breakline code (0 to 7). A void or hole triangle (-2, -1) or an undocumented code is a typed Unsupported refusal; a Flags list whose length differs from the triangle count is Degenerate. solid::tessellated::TriangulatedIrregularNetwork is the borrowed view. BodyKind::classify and SolidKind::classify report it as tessellated.
  • lower::dispatch::SPECIALISATIONS (and Specialisation): subtypes routed to their supertype's lowering, each naming the attributes it adds. A test checks every row against the IFC4X3 schema.
  • tests/schema_coverage.rs also enumerates IFC4X3 ADD2. Its inventory is checked against the bundled table and IFC4X3_ADD2.exp. Every IFC4X3 representation item must be in IMPLEMENTED, in PLANNED, or have a nested disposition. Every IFC4X3 profile must be read or in UNLOWERED.
  • Fixtures synthetic_ifc4x3_alignment_curves.ifc and synthetic_ifc4x3_geometry_families.ifc, with their generators tools/gen_ifc4x3_curve_fixtures.py and tools/gen_ifc4x3_geometry_fixtures.py.

Changed ​

  • lower::dispatch::PLANNED lists the IFC4X3 representation items that are not lowered, each with its runtime refusal text, instead of the generic "representation item family is not lowered yet":
    • a spiral or IfcPolynomialCurve on its own (unbounded; each lowers as the ParentCurve of an IfcCurveSegment);
    • IfcSegmentedReferenceCurve (cant has no neutral roll law, #93);
    • IfcSectionedSolidHorizontal and IfcSectionedSurface (no neutral sectioned sweep over curve-measure stations, nor a sectioned-surface relation);
    • IfcOffsetCurveByDistances, IfcPointByDistanceExpression and IfcAxis2PlacementLinear (no neutral distance-along-curve relation).
  • Further typed refusals, recorded in dispatch::PARTIAL: an IfcPolynomialCurve parent trimmed by arc length and a parabola without a stated end (#90); IfcAxis2PlacementLinear segment placements (#93); vertical arcs and clothoids (#258); IfcParameterValue measures; plan kinks, gaps and profiles that do not span the plan.

Fixed ​

  • An IfcCurveSegment reached through a path that cannot lower it (a sweep StartParam/EndParam range, a p-curve, a profile boundary) is refused by name. Before, the IfcCompositeCurveSegment slots were read, so SegmentLength was taken for ParentCurve and the result was a misleading wrong-value-kind error. curve::composite::CURVE_SEGMENT_UNREAD is the view's backstop refusal.
  • constraint::placement::derive_placement_transform resolves an IfcLinearPlacement whose basis curve is an IfcGradientCurve through the gradient-curve lowering. Before, it passed the curve to ifc_alignment::gradient_curve3, which expects an IfcAlignment, so the path always refused.
  • The dispatch::PLANNED documentation claimed every recognized representation item is lowered. It now lists the IFC4X3 families that are not.

[0.6.0] - 2026-10-03 ​

ifc-alignment ​

Input this crate refused now lowers exactly, onto the Axiolid relations of axiolid-curve 0.3.3 and axiolid-model 0.3.4 (kernel#238, #239, #240), and HorizontalPlan::curve can now be a Curve2::Chain: behaviour changes, so the next release is a minor one (0.6.0).

Added ​

  • lower_segmented_reference_curve lowers an alignment with cant to an exact Curve3::Banked instead of refusing it, and segmented_reference_curve3 returns that curve directly (#93). The cant law is D = left - right and the pivot (left + right) / 2, one CantPiece per cant segment in the IFC4.3 base formula (the Helmert transition as its two halves, the Viennese bend as its bank angle); the section rolls about the 3D tangent by arcsin(D / b) (BankConvention::TangentRotation, the reading IFC4.3 ADD2 states).

Changed ​

  • A CUBIC horizontal segment lowers on every path (#90): per segment and in the composite as its exact cubic Bezier trimmed at TrimSelector::ArcLength(SegmentLength), and in the plan as a ChainPiece2::Parametric read by arc length, which makes lower_horizontal_plan return a Curve2::Chain for a layout holding one. The seam after a CUBIC reports its position (and heading) as SeamCheck::Authored.
  • A vertical CIRCULARARC lowers to ElevationLaw::CircularArc from StartHeight, StartGradient and the signed RadiusOfCurvature, on the per-segment path as the circle itself trimmed by angle (#258). A radius turning against the authored grades, or an arc turning vertical before its end, is InvalidSegment.
  • A profile starting before the plan rebases a straddling circular arc exactly, as it already rebased a polynomial piece.

Still refused, by name ​

  • A vertical CLOTHOID: its segment states neither end curvature (RadiusOfCurvature is defined for arcs and parabolas only), so its law is undetermined.
  • A CUBIC that starts curved: IFC4.3 defines only y = x^3 / (6 R L) leaving a straight.
  • A pivot that moves through a Viennese bend (Axiolid's BankError::AngleInPivot), a cant layout that does not span the plan, and |D| > b.

ifc-geometry ​

Changed (breaking) ​

  • Requires ifc-alignment 0.5. derive_placement_transform takes an ifc_alignment::PointByDistance, so the alignment version is part of this crate's public API; mixing it with ifc-alignment 0.4 types no longer compiles. No behaviour changes in this crate.

ifc-georef ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with georeferencing (and the property sets it needs) shrinks from 1,583,507 to 990,688 bytes. The default build links every release, as before.
  • GeorefView::for_model refuses an IFC4 or IFC4X3 file whose table the build leaves out with GeorefError::UnsupportedSchema. The semantic column (--no-default-features) therefore names its releases too, e.g. --no-default-features --features ifc4,ifc4x3.

Under 0.x, a minor release.

ifc-properties ​

Changed (breaking) ​

  • Comparison is #[non_exhaustive]: a match needs a wildcard arm.
  • The read results ExactProperty, ExactPropertyEntry, ExactTableRow, Property, PropertySet, QuantitySet, ResolvedSet and PropertySetTemplate are #[non_exhaustive]; compare their fields instead of building one with a struct literal.
  • Every authoring draft is #[non_exhaustive], so a struct literal no longer compiles outside the crate. Each gains new(required…) and one builder setter per other field, named after the field and taking the unwrapped value: TableValueDraft::new(name), DoorLiningDraft::new(), WindowLiningDraft::new(), ReinforcementBarDraft::new(total_cross_section_area, steel_grade), SectionReinforcementDraft::new(longitudinal_start_position, longitudinal_end_position, reinforcement_role, section_definition, cross_section_reinforcement_definitions), SiUnitDraft::new(unit_type, name), MonetaryUnitDraft::new(currency) and ConversionBasedUnitDraft::new(unit_type, name, conversion_factor, dimensions). Fields stay public.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • The unique-member-name rule of complex properties and quantities is labelled per verified release only; another release is refused with UnsupportedSchema rather than given the IFC4 label.
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.
  • Exact value checks treat a type declaration form ifc-schema adds later as not matching; follows ifc_schema::TypeKind becoming #[non_exhaustive].

ifc-validate ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a build naming only ifc4 no longer carries the other four: an IFC4-only browser module with validation shrinks from 1,518,692 to 926,023 bytes. The default build links every release, as before. ifc4 alone used to link every release too; it now links IFC4's, and validate_declared refuses a file declaring another release with UnbundledSchema. Under 0.x, a minor release.
  • validate_declared is no longer gated on the ifc4 feature: it exists in every build and refuses a release the build does not bundle.

openbim-ifc ​

Changed (breaking) ​

  • properties re-exports ifc-properties 0.4.0, whose breaking changes (PropertyAnomaly is #[non_exhaustive]; template_of_set returns every template of a set) pass through. It also resolves quantity sets in exact_property instead of reporting them absent (#66).

Changed ​

  • STEP models load lazily: from_step_bytes, read_path and every strict read validate the whole file but decode each entity on first access (ADR 0015, see ifc-step). read_path hands its buffer to the codec instead of letting it copy the file once more.

Added ​

  • StepReader, ParseOptions and OnMalformed are re-exported, so the eager and memory-mapped reads (StepReader::eager, StepReader::read_path_mapped) are reachable through the facade.

Added ​

  • Transaction, Applied and Conflict are re-exported. EntityEditor and the domain writers stage into a Transaction, which facade users could not name, so an editor could be built but never applied.

[0.5.3] - 2026-09-28 ​

ifc-properties ​

Added ​

  • exact_material_property, exact_material_properties_where and exact_material_property_sets_where (#218): exact readers for the property sets of a material definition, as exact_property and its enumerations read an object's. IFC4 and IFC4X3 IfcMaterialProperties (the inverse HasProperties) and IFC2X3 IfcExtendedMaterialProperties resolve their named properties; an IFC2X3 typed subtype (IfcGeneralMaterialProperties, ...) resolves its attributes as a predefined set does, keyed by its entity name. Results carry the new ExactSource::Material; a material without sets is a proven absence, and a target that is no material definition of the release is InvalidQueryObject.

[0.5.2] - 2026-09-28 ​

ifc-properties ​

Added ​

  • *_with_owner_history variants of the predefined property-set writers (#202): add_door_lining_properties_with_owner_history, add_window_lining_properties_with_owner_history, add_door_panel_properties_with_owner_history, add_window_panel_properties_with_owner_history, add_permeable_covering_properties_with_owner_history and add_reinforcement_definition_properties_with_owner_history. Each takes the model and a caller-supplied IfcOwnerHistory, which IFC2X3 requires on every IfcRoot, validated as for #191 (MissingEntity, AuthoringInvalid); none is ever invented. The record is laid out by attribute name from the declared release's table, and each value is checked against the type that release declares for it. In IFC2X3 that refuses LiningToPanelOffsetX/Y (and a window's LiningOffset) with AuthoringNotInSchema, and a zero thickness, which IFC2X3 types as IfcPositiveLengthMeasure where IFC4 has IfcNonNegativeLengthMeasure, with AuthoringInvalid; a token outside the release's enumeration is refused too. In IFC4 and IFC4X3 the record is the old writer's with the owner history in its optional slot.

  • *_with_owner_history variants of the template writers (#202): add_property_set_template_with_owner_history, add_complex_property_template_with_owner_history and attach_template_with_owner_history. They bind the model's release, so an IFC2X3 model, which declares no templates, is refused with EntityNotInSchema.

  • ExactValue::Complex with ExactComplexValue and ExactComplexMember (#208): exact_property and the enumerations resolve an IfcComplexProperty or IfcPhysicalComplexQuantity as a present composite in IFC2X3, IFC4 and IFC4X3, with no value type, instead of refusing it with UnsupportedProperty. Members resolve as set members do, nested complexes included. New errors ComplexCycle, ComplexTooDeep and ComplexBudgetExceeded refuse a cycle, nesting past 16 levels and more than 10 000 nested members; a repeated member name is InconsistentValues where the release forbids it (WR22, UniqueQuantityNames).

Changed ​

  • The predefined property-set and template writers that take no model (add_door_lining_properties, ..., add_reinforcement_definition_properties, add_property_set_template, add_complex_property_template, attach_template) are unchanged: they write the IFC4 layout with OwnerHistory $, which is valid IFC4 and IFC4X3 (whose layouts of these entities are the same) and never valid IFC2X3. Without a model they cannot refuse IFC2X3; their documentation now says so, as #191 did for add_property_set. They now lay the IFC4 record out by attribute name from the IFC4 table instead of fixed slots; the output is identical. The lining writers moved to pset/lining.rs and add_complex_property_template to pset/template_authoring.rs; public paths are unchanged.

[0.5.1] - 2026-10-20 ​

ifc-georef ​

Added ​

  • ProjectToMap::declared_map_unit(): the target IfcProjectedCRS.MapUnit exactly as authored, None when the file leaves it unset, next to the resolved ProjectToMap::map_unit, which keeps the project length unit as the default for an omitted MapUnit and is what the transform uses. A CRS stating the project's unit is now distinguishable from one stating none, under IFC4 and IFC4X3, with or without the transform feature (#296).

Changed ​

  • Documented that ProjectedCrs::map_unit is the declared MapUnit (None when unset), never filled in with the project default (#296).

ifc-properties ​

Added ​

  • add_property_set_with_owner_history, add_element_quantity_with_owner_history, attach_property_set_with_owner_history and attach_type_with_owner_history (#191). Each takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and must be an IfcOwnerHistory; a missing one is refused with MissingEntity, another entity with AuthoringInvalid. None is ever invented. The record is laid out by attribute name from the declared release's table, and in IFC4 and IFC4X3 the reference is written into the optional slot. This follows ifc-material's associate_material_with_owner_history (#77).
  • PropertyError::AuthoringRequired { entity, attribute, schema }: the model's release requires an attribute the call leaves unset (#191). PropertyError is #[non_exhaustive], so this is not breaking.

Changed ​

  • Type objects are no longer refused by the exact API (#193). exact_property, exact_properties, exact_properties_where, exact_property_sets_where and exact_predefined_sets answered an IfcTypeObject with ExactPropertyError::InvalidQueryObject, so a checker could not test an IfcWallType as IDS does. They now resolve the type object's own HasPropertySets: property sets, quantity sets and predefined sets, in IFC2X3, IFC4 and IFC4X3. Every subtype of the release's IfcTypeObject is accepted, IFC2X3 IfcDoorStyle and IfcWindowStyle included. The list is read and validated by the code that already reads an occurrence's inherited type sets. A result carries ExactSource::Type(id) with the queried object's id, which is what an occurrence of that type reports for the same set. HasPropertySets = $ is a proven absence; () is refused with MalformedAggregate, and duplicate names with DuplicateMatchingSets { source: Type(id), .. } and DuplicateMatchingProperties, as for an inherited type. A type object named in IfcRelDefinesByProperties.RelatedObjects is still refused with InvalidOccurrenceTarget, also when it is the queried object: IFC2X3 admits only IfcObject there, and IFC4 and IFC4X3 forbid it by NoRelatedTypeObject ("handled through the direct relationship HasPropertySets at IfcTypeObject"). Occurrence results are unchanged. No type or signature changes; callers that relied on the refusal now get answers, and InvalidQueryObject remains for what is neither an occurrence nor a type object. In openbim-ifc, door_operation and window_operation given a type object now refuse with NotADoor or NotAWindow instead of Property(InvalidQueryObject).

Fixed ​

  • Quantity values are written bare (#190). create_quantity, create_quantity_with and set_quantity_value wrote IFCQUANTITYAREA('A',$,$,IFCAREAMEASURE(12.5),$). But <Kind>Value is declared with a defined measure type (IfcAreaMeasure, ...), not a SELECT, in IFC2X3, IFC4 and IFC4X3, and ISO 10303-21 writes a typed parameter only in a SELECT slot. They now write 12.5 (a whole count as the integer 4). set_quantity_value replaces a typed value it finds with the bare one. The readers (quantity_set, exact_property) still accept both forms. Code that inspects the written Value sees Value::Real/Value::Integer where it saw Value::Typed.
  • IfcRoot.OwnerHistory is no longer written as $ into an IFC2X3 model by attach_property_set and attach_type (#191). It is mandatory in IFC2X3 (OPTIONAL from IFC4 on), so those records were invalid. Behaviour change for IFC2X3 callers: both now refuse an IFC2X3 model with PropertyError::AuthoringRequired { attribute: "OwnerHistory", .. } and stage nothing; use the *_with_owner_history variants there. Both now bind the model's declared release the way quantity authoring does. So a model whose header declares several schemas, or one without a bundled table, is refused with MultipleSchemas or UnsupportedSchema where it used to be written in the IFC4 layout. NoRelatedTypeObject and the IfcTypeObject checks use the declared release's inheritance instead of IFC4's. IFC4 and IFC4X3 output is unchanged. add_property_set and add_element_quantity take no model, so they cannot see the release. They still write $ and are documented as IFC4/IFC4X3 only. In IFC2X3, attach_property_set_with_owner_history refuses to attach a definition whose OwnerHistory is unset. Not changed: the template writers, whose entities IFC2X3 does not declare, and the predefined property-set writers, which write the IFC4 layout without a model.

ifc-spatial ​

Documentation ​

  • contain, contain_with_owner_history, serve_buildings and serve_buildings_with_owner_history are documented as the canonical writers; the ifc-systems duplicates are deprecated (#492).

Semver: patch. Documentation only.

ifc-style ​

Fixed (#514) ​

Semver: patch. No public item changes.

  • FillAreaStyle::fill_styles accepts a solid colour fill. It listed the IfcColour select itself as a member of IfcFillStyleSelect, and a select name never matches an instance's entity type, so every IfcColourRgb (and IfcDraughtingPreDefinedColour) fill was refused with StyleError::ReferenceType. It now lists IfcColour's own members, IfcColourSpecification and IfcPreDefinedColour, as every bundled release declares them; the colour then reads through StyleView::colour_rgb. A non-colour member is still refused.

[0.5.0] - 2026-10-13 ​

ifc-alignment ​

Added ​

  • lower_horizontal_plan lowers a whole IfcAlignmentHorizontal to one exact Curve2::Intrinsic: the first segment's start frame and a CurvatureLaw::Piecewise with one piece per segment. HorizontalPlan carries it with its sources and seams (#92).

  • HorizontalSeam and SeamCheck report every seam between horizontal segments and whether its position was verified in closed form or accepted as authored after a transition spiral. LoweredAlignmentCurve::seams carries them for layout and gradient lowerings (#239).

  • AlignmentView::hierarchy returns an AlignmentHierarchy: the horizontal, vertical and cant layouts and the IfcReferents an IfcAlignment nests (IfcRelNests, in nesting order), its parent and child alignments (IfcRelAggregates), and the products it positions (IfcRelPositions). sole_horizontal, sole_vertical and sole_cant refuse several layouts of a kind instead of picking one; governing_horizontal follows a child alignment to the horizontal layout its parent nests; layout_segments lists a layout's parameter segments; alignments and model round out the view (#238).

  • VerticalLayout::resolve, the vertical counterpart of CantLayout::resolve: the ordered segments of an IfcAlignmentVertical, refusing a gap or overlap (#238).

  • Stationing::resolve scopes station equations to one alignment, from the referents it nests or positions, and checks each IncomingStation (or a plain continuation) against the station carried from the previous referent within STATION_TOLERANCE. station_at, distance_at and distances_at map between station and distance along across equations and decreasing stationing; a station that occurs twice is AlignmentError::AmbiguousStation, one outside the table AlignmentError::OutOfRange (#240).

  • The IFC4X3 ADD2 declaration and slot inventory is pinned by tests against the bundled schema table: every slot and arity the readers and authoring index, the declarations read, the subtype and SELECT memberships relied on, every segment PredefinedType member, and the absence of the layouts from IFC2X3 through IFC4X2 (#16).

  • vertical_profile_law(model, vertical, units): the exact profile of an IfcAlignmentVertical, with its seams checked at the precision the file declares (#141).

  • SeamTolerance and profile_law_within: the seam tolerance as a value. SeamTolerance::for_model reads Precision from the model's 3D IfcGeometricRepresentationContexts (the coarsest, converted from the project length unit, capped at 1 mm); profile_law keeps the rounding-only rule (#141).

  • CantLayout::for_alignment: the alignment's sole cant layout; none or several are a SemanticViolation (#93).

  • CantLayout::frame_at_distance and CantFrame: cant at a station as rail heights, cant D = left - right, bank angle arcsin(D / b), the rotation-point elevation (left + right) / 2 and the section frame; CantFrame::orient places it on a caller-evaluated point and tangent (#93).

  • lower_segmented_reference_curve: the cant-carrying centreline (IfcSegmentedReferenceCurve role). It resolves the cant layout and then refuses with Unsupported, because the pinned neutral curve vocabulary has no roll law (#93).

  • VerticalLayout::seams lists every seam between vertical segments of positive length as a VerticalSeam (previous and next segment, station, incoming and outgoing grade) with a VerticalSeamKind: Tangential or GradeBreak. VerticalLayout::require_tangential refuses the first grade break with ProfileDiscontinuity { seam: ProfileSeam::Gradient } for a consumer that needs a tangent profile (#259).

  • real_export_survey (ignored test): runs every alignment path over a local directory of real IFC4X3 exports (IFC_ALIGNMENT_EXPORTS) and tallies acceptances and refusals by cause.

Changed ​

  • lower_gradient_curve and gradient_curve3 elevate a multi-segment plan instead of refusing it, using lower_horizontal_plan. The plan is now always that intrinsic curve, also for a single segment, where it used to be the segment's basis line, circle or spiral. A heading kink between segments is refused, since one intrinsic curve cannot carry it (#92).

  • lower_horizontal_layout lowers a layout containing transition spirals to one composite instead of refusing it. A seam after a spiral is accepted as authored, reported in seams, and declares Transition::Discontinuous (no claim); a gap after a line or arc is still refused. lower_horizontal_layout_partial no longer splits runs at spirals, so PartialHorizontalLayout::is_complete again means one run (#239).

  • A CUBIC horizontal segment is refused with its own reason: its end on the curve inverts a non-elementary arc-length integral, and the pinned neutral vocabulary has no arc-length trim. A CUBIC with a non-positive length, non-finite radii or equal start and end radii is reported as InvalidSegment first (#90, still open).

  • lower_vertical_segment lowers through elevation_law, like the composed gradient curve, so both paths accept and refuse the same segments. PARABOLICARC now lowers exactly to a quadratic Bezier in the (distance along, height) plane parameterised by plan distance; it was refused (#91).

  • Vertical CIRCULARARC and CLOTHOID stay a typed Unsupported refusal, now naming the family and why it has no polynomial law; exact lowering waits on an Axiolid elevation law (#91, #258).

  • lower_gradient_curve and gradient_curve3 check vertical seams at the model's declared precision instead of rounding only, so files whose exporter rounds StartDistAlong or StartHeight to that precision compose (buildingSMART's BC003_* alignment reference datasets did not) (#141).

  • A CONSTANTGRADIENT segment may carry RadiusOfCurvature = 0. and start and end gradients equal up to rounding, as real exports write them; both were refused.

  • VerticalLayout::resolve checks contiguity with the same SeamTolerance rule as vertical_profile_law, at the model's declared precision (#141).

  • Every layout path accepts the zero-length segment IFC4.3 requires at the end of each horizontal, vertical and cant layout (concept template Alignment Layout - Horizontal, Vertical and Cant). It adds no geometry and no curve piece; its start is still checked against the end of the previous segment by that layout's seam rule (closed-form position and heading, station and height, station and cant), and the horizontal seam is reported last in seams. A zero-length segment anywhere else, or as a layout's only segment, is refused with SemanticViolation; the cant and vertical paths accepted one anywhere before, and the horizontal reader refused every one. Covers lower_horizontal_plan, lower_horizontal_layout, lower_horizontal_layout_partial, profile_law/profile_law_within, vertical_profile_law, VerticalLayout::resolve, CantLayout::resolve and the gradient curve (#262).

  • read_horizontal_segment reads SegmentLength = 0 (the schema type is IfcNonNegativeLengthMeasure); only a negative length is refused (#262).

  • lower_horizontal_segment and lower_vertical_segment refuse a zero-length segment with InvalidSegment: on its own it has no geometry. A zero-length CONSTANTGRADIENT lowered to a degenerate line before (#262).

  • A grade break at a vertical seam whose height is continuous is accepted by profile_law, profile_law_within, vertical_profile_law, VerticalLayout::resolve and the gradient curve. IFC4.3 ADD2 does not require vertical seams to be tangential (IfcAlignmentVerticalSegment), and real exports carry such breaks (buildingSMART BC003_ALX2, Trimble). The piecewise elevation law carries the kink exactly: each piece keeps its own grade. A height step beyond SeamTolerance is still refused, and ProfileSeam::Gradient is now produced only by require_tangential (#259).

Deprecated ​

  • station_equations, which merges the referents of every alignment in the model into one table. It keeps working; use Stationing::resolve (#240).

Fixed ​

  • A clockwise CIRCULARARC plan elevated through lower_gradient_curve ran backwards: the evaluator reads a circle plan's distance counter-clockwise, so a negative radius was traversed in reverse. The intrinsic plan carries the signed curvature instead.
  • lower_gradient_curve and gradient_curve3 read the vertical profile at its own stations. StartDistAlong is measured from the start of the horizontal layout, but the profile was read from plan distance 0, so a profile starting at station s put every height s metres early. A profile starting before the plan is now re-indexed exactly (Taylor shift of the straddling piece); one starting after the plan start, or ending before the plan ends (beyond the seam tolerance), is refused with Unsupported, because the composed curve has no domain to leave those stations without heights.

ifc-geometry ​

Changed ​

  • MaterialProfileSetUsageGeometry::new accepts IfcMaterialProfileSetUsageTapering, the schema subtype of IfcMaterialProfileSetUsage, whose inherited slots it reads unchanged; the new MaterialProfileSetUsageGeometry::tapering() returns its MaterialProfileSetUsageTaperingGeometry (end profile set and end cardinal point), or None for a plain usage (#136).

Changed (breaking) ​

  • ViolationKind, Support and FunctionStatus are #[non_exhaustive]: a match needs a wildcard arm.
  • RuleViolation, LoweredGeometry and MappedInstance are #[non_exhaustive]; they can no longer be built with a struct literal outside the crate.
  • authoring::surface_curve_swept_area_solid, authoring::fixed_reference_swept_area_solid and authoring::swept_disk_solid are removed (#210). Without the model they could not write every release correctly: the two directrix sweeps wrote the IFC4X3 IFCPARAMETERVALUE(..) trim into IFC4 files, and the swept disk wrote $ for the trim IFC2X3 requires. Use surface_curve_swept_area_solid_in, fixed_reference_swept_area_solid_in and the new swept_disk_solid_in, which take &Model after the transaction and otherwise the same arguments.
  • SurfaceCurveSweptAreaSolid::start_param/end_param and FixedReferenceSweptAreaSolid::start_param/end_param return GeometryResult<Option<TrimMeasure>> instead of Option<f64> (#210). In IFC4X3 the trim is an IfcCurveMeasureSelect, and an IFCLENGTHMEASURE(..) trim is a distance along the directrix, not a curve parameter; the reader now says which (TrimMeasure::Parameter or TrimMeasure::Length) instead of returning both as a parameter. A bare number is a parameter, as IFC2X3 and IFC4 declare. A typed value that is not one of the SELECT's members is refused with WrongValueKind instead of being unwrapped.
  • Lowering an IfcSurfaceCurveSweptAreaSolid or IfcFixedReferenceSweptAreaSolid whose trim is an IfcLengthMeasure fails with Unsupported (#210). It used to pass the length on as a curve parameter, which on a conic directrix reads metres as radians; converting a length into the directrix's parameter needs arc-length evaluation, which lowering does not do.

Added ​

  • authoring::swept_disk_solid_in (#210): an IfcSweptDiskSolid in the model's declared release. The trim is written bare in every release, as before; in IFC2X3, which declares StartParam and EndParam required, an unset one is refused with InvalidAuthoredValue and nothing is staged.
  • solid::swept::TrimMeasure (re-exported from solid), the kind and value of a directrix sweep's trim, and TrimMeasure::parameter.

Fixed ​

  • authoring::curve_segment writes a CurveMeasure::Length as IFCLENGTHMEASURE(..) (#210). It wrote IFCNONNEGATIVELENGTHMEASURE(..), which is not a member of IFC4X3 IfcCurveMeasureSelect = SELECT (IfcLengthMeasure, IfcParameterValue) and is an ifc-validate error. Because IfcLengthMeasure is signed and neither slot is bounded, a negative length is now written as given instead of refused.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-georef ​

Added ​

  • site_reference reads IfcSite.RefLatitude, RefLongitude and RefElevation (metres, through the project length scale) under IFC2X3, IFC4 and IFC4X3 into SiteReference; compound angles are validated against the declared release's own WHERE rules and the WGS84 range, and convert to decimal degrees via CompoundPlaneAngle (#242).
  • relate_site_elevation compares RefElevation with the map height of the site origin (OrthogonalHeight plus the scaled site-origin height) and returns SiteElevationCheck: a disagreement beyond the tolerance (SITE_ELEVATION_TOLERANCE_M, 1 cm suggested) is a reported finding carrying both values and the target CRS's VerticalDatum, never a silent pick (#242).
  • GeorefError::InvalidCompoundAngle and GeorefError::InvalidParameter (#242).
  • IFC4X3 IfcRigidOperation with IfcLengthMeasure coordinates resolves to a translation through resolve_project_to_map(_in): the offsets and Height are in the target MapUnit, and the linear part is the identity in metres (#241). WHERE SameCoordinateType is checked; an untyped REAL, mixed or non-length/non-angle coordinates are refused with GeorefError::RuleViolation { rule: "SameCoordinateType" }, and a length operation onto a non-projected target with WrongType.
  • resolve_geographic_offset_in and GeographicOffset: an IFC4X3 plane-angle IfcRigidOperation read as authored onto its IfcGeographicCRS (#241). The project-to-map resolver refuses that form with the new GeorefError::CoordinateMeasureMismatch, since no metre transform expresses a latitude/longitude offset.
  • GeographicCrs and AngleUnit: IFC4X3 IfcGeographicCRS with PrimeMeridian, AngleUnit (PLANEANGLEUNIT) and HeightUnit (LENGTHUNIT); an absent unit stays None (#241).
  • create_angular_rigid_operation, writing IFCPLANEANGLEMEASURE(..) coordinates (#241).
  • OperationSource, resolve_operation_source and coordinate_operation_for: the operation's SourceCRS validated as an IfcCoordinateReferenceSystemSelect member of the pinned release, and the HasCoordinateOperation inverse read from a context or CRS (#101).
  • ProjectToMap.source, .operation and .kind (OperationKind).
  • A default transform feature carrying the axiolid-core dependency (#268). With default-features = false the crate links no geometry crate and still resolves CRS metadata and units, every map-conversion and rigid-operation parameter, true and grid north, operation sources and the site reference. tests/kernel_free_build.rs asserts the resolved dependency graph, and the gate builds, tests, lints and documents the column.
  • ProjectToMap.eastings, .northings and .orthogonal_height, as authored in map_unit (a rigid operation's FirstCoordinate, SecondCoordinate and Height, 0.0 when unstated), and the plain-number operation: ProjectToMap::map_point (project metres to map metres, bit-identical to transform.transform_point3), ProjectToMap::linear_part and ProjectToMap::translation (#268).

Changed ​

  • ProjectToMap.transform and compose_project_frame exist only with the transform feature, which is on by default, so a default build is source-compatible (#268). relate_site_elevation computes through ProjectToMap::map_point and is available in both columns, with the same results.
  • IfcMapConversionScaled resolves instead of being refused with UnsupportedOperation: FactorX/Y/Z scale the source axes before the rotation, and a non-positive factor is refused with InvalidAttribute (#241). grid_north_direction accounts for unequal FactorX/FactorY.
  • create_rigid_operation writes its coordinates as IFCLENGTHMEASURE(..) instead of bare REALs, which cannot satisfy SameCoordinateType (#241).
  • Every resolved operation now validates its SourceCRS (#101): a dangling reference is MissingEntity, a wrong type WrongType, a sub-context RuleViolation { rule: "NoCoordOperation" }, and a source named by more than one operation RuleViolation on the HasCoordinateOperation : SET [0:1] inverse. Files that resolved with such a source before are now refused.
  • resolve_project_to_map pins the header's release when it names IFC4 or IFC4X3, so IFC4X3-only entities in an IFC4 file are refused as undeclared by both entry points. A non-coordinate-operation id is WrongType { expected: "IFCCOORDINATEOPERATION" } (was "IFCMAPCONVERSION").
  • create_map_conversion and create_map_conversion_scaled refuse a negative Scale, and create_map_conversion_scaled a negative FactorX/FactorY/FactorZ, with AuthoringInvalid naming the attribute and nothing staged (#254). Zero and non-finite values were already refused. The reader refuses all of these (InvalidScale, InvalidAttribute), so a caller that passed one got a record this crate could not read back; such calls now fail at authoring time.

ifc-material ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with materials shrinks from 1,445,151 to 852,448 bytes. The default build links every release, as before.
  • A release the build leaves out is refused with UnsupportedSchema instead of panicking; so is the IFC4 baseline of a model with no header in a build without IFC4. A build without default features therefore has to name the releases it reads.

Under 0.x, a minor release.

ifc-properties ​

Changed (breaking) ​

  • quantity_set, quantity_sets and complex quantities list a simple quantity whose value is $, cut off by a truncated record, or not a number, as the new Quantity::Unresolved (#138). It sits in file order beside the others and carries the quantity's id, Name, Description, kind, stated unit and Formula, and a reason: the new UnresolvedValue::Missing or UnresolvedValue::NotNumeric { found }. It has no value field, so nothing can read it as 0. Since #107 such a quantity was reported but left out of quantities, so a caller listing a set could not see that it existed. PropertyAnomaly::QuantityValueMissing and QuantityValueNotNumeric are still reported, once each.

  • Quantity is #[non_exhaustive]. Exhaustive matches need an arm for Unresolved and a wildcard arm. UnresolvedValue is #[non_exhaustive] too.

  • compare returns Comparison::NotComparable for an unresolved quantity, and stated_unit returns its stated unit.

  • A quantity without a readable value is now checked against WR21: a stated unit of the wrong kind is reported as PropertyAnomaly::QuantityUnitMismatch, as it is for a valued quantity.

  • New QuantityKind::Number for IFC4X3 IfcQuantityNumber (#138), and QuantityKind is #[non_exhaustive]. Exhaustive matches need an arm for Number and a wildcard arm. In a model whose declared release is IFC4X3, the permissive readers now resolve IfcQuantityNumber as a Simple quantity, where they returned Quantity::Unsupported. NumberValue and Formula are located by name in that release's table. A $ or non-numeric value is Quantity::Unresolved, and the anomalies are reported as for the other kinds. Its measure is IfcNumericMeasure. It has no WHERE rule, so there is no unit-kind check (required_unit is None) and a negative number is not a NegativeQuantity. compare compares it only against a computed Number, and stated_unit returns whatever unit it states. IFC2X3 and IFC4 do not declare the entity. In their models, and in a model without one known declared release, it stays Quantity::Unsupported.

  • Quantity authoring binds to the model's declared release (#138). create_quantity(tx, model, kind, name, value) and create_quantity_with(tx, model, kind, name, value, extras) take the &Model they write into and return Result<EntityId, PropertyError>. They used to write the IFC4 five-attribute record into every model, so an IFC2X3 quantity got a Formula slot IFC2X3 does not declare. Attributes are now placed by name in the release's table: IFC2X3 quantities have four attributes, IFC4 and IFC4X3 ones five. The binding is the one ifc-material uses. One recognised FILE_SCHEMA binds that release, and a model with no FILE_SCHEMA binds IFC4. Several declarations are refused with the new PropertyError::MultipleSchemas, and an unknown one with UnsupportedSchema. Also refused:

    • QuantityKind::Number outside IFC4X3, with EntityNotInSchema;
    • a Formula for an IFC2X3 model, with AuthoringNotInSchema;
    • a non-finite value, with AuthoringInvalid.

    A count is no longer truncated. A whole count is still written as an integer. A fractional one is written as a real where IfcCountMeasure is NUMBER (IFC2X3, IFC4), and refused where it is INTEGER (IFC4X3). set_quantity_value binds the same way. It writes the value slot named in the release, refuses a record whose attribute count is not the release's with MalformedEntitySlots, and repairs a $ value (a Quantity::Unresolved) into a simple quantity. Nothing is staged on a refusal. The new PropertyError variants are additive, since the enum is #[non_exhaustive]; the signatures are the breaking part. add_quantity_to_set, add_element_quantity and add_physical_complex_quantity are unchanged, because IfcElementQuantity and IfcPhysicalComplexQuantity have the same attributes in all three releases.

The exact API is unchanged and agrees: exact_property refuses a $ value with MissingValueSlot, a non-numeric one with UnsupportedValue and a truncated record with MalformedEntitySlots, in IFC2X3, IFC4 and IFC4X3.

  • PropertyTemplate reads every attribute of both template entities by name from the declared release's table (#108) and gains kind (PropertyTemplateKind::Simple or Complex), enumerators, secondary_unit, expression, access_state, usage_name and templates (the nested HasPropertyTemplates of a complex template, read through the same bounded, cycle-aware traversal as complex properties, with template(name) to look one up). It is now #[non_exhaustive], so code that builds it with a struct literal or destructures it exhaustively must change; later fields will not break callers again.

Added ​

  • exact_property_sets_where(model, object, select_set) and ExactPropertySetEntry { name, set_id, source, members }: the property sets and quantity sets an object carries whose names the selector picks, empty ones included (#186). exact_properties_where lists properties, so a matching set without any left no trace; an IDS property facet must fail on exactly that set. The traversal, type-over-occurrence order and refusals are those of exact_properties_where, except that a HasProperties or Quantities of () or $ (invalid, both are SET [1:?]) is listed with members == 0 instead of refused: the question is whether the set exists. A type set with an occurrence set's name is listed as well, since overriding works per property.

  • template_deviations(model) compares every property set that an IfcRelDefinesByTemplate links to an IfcPropertySetTemplate with that template (#109), reading the model and its templates only, bound to the declared release (IFC4 or IFC4X3; IFC2X3 has no templates and is refused with TemplateError::NoTemplates). It returns a TemplateReport of TemplateFindings, each naming the set, the template and the concrete mismatch: MissingProperty, UnexpectedProperty (matched by Name, as the IFC4 IfcPropertySetTemplate documentation states), WrongForm (the property's entity against the template's TemplateType, e.g. a single value where P_ENUMERATEDVALUE is prescribed), WrongMeasureType (PrimaryMeasureType, and SecondaryMeasureType for bounded and table values, against the declared type of each value or the referenced entity), WrongSetKind (a QTO_* template on an IfcPropertySet, a PSET_* one on an IfcElementQuantity), WrongAttachment (a *_TYPEDRIVENONLY set on an occurrence, an *_OCCURRENCEDRIVEN set on a type) and OutsideApplicableEntity (IfcEntity[/PREDEFINEDTYPE] entries, comma separated). Complex properties are compared with complex templates member by member. Quantity sets are checked the same way with Q_* templates. What the documentation leaves open is TemplateFinding::Undecided with an UndecidedReason, never guessed: an unknown or undocumented template type (IFC4X3 Q_NUMBER), an unknown measure type or ApplicableEntity entry, an object whose predefined type is unstated, a [PerformanceHistory] entry, a predefined property set. Malformed facts met on the way are PropertyAnomalys in the same report. New types: TemplateReport, TemplateFinding, MeasureRole, UndecidedReason, TemplateError, all #[non_exhaustive].

  • property_template_checked and property_set_template_checked read a template bound to the declared release and report every malformed fact met (#108), refusing a model without a single supported release, an IFC2X3 model (TemplateError::NoTemplates), an absent entity and a non-template with TemplateError.

  • PropertyAnomaly::NotATemplate, PropertyAnomaly::SlotCountMismatch and PropertyAnomaly::MalformedAttribute (#108, #109): a template member or RelatingTemplate that is no template, a record whose attribute count is not the release's, and an attribute value its declared type does not admit (including an enumeration constant the release does not define). PropertyAnomaly is #[non_exhaustive], so this is not breaking.

Fixed ​

  • property_template read every template with the IfcSimplePropertyTemplate layout (#108), so an IfcComplexPropertyTemplate, including one written by add_complex_property_template, reported its UsageName as its TemplateType and never exposed its nested templates; and any entity at all read as a template. It now returns None for an entity that is not a simple or complex template in the declared release (the IFC4 table when the header names none it bundles, and nothing in an IFC2X3 file). property_set_template, property_set_templates and template_of_set read their attributes by name the same way.

ifc-spatial ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table. The default build links every release, as before. Container classification needs IFC2X3, IFC4 or IFC4X3: a build naming none of them would classify nothing as a container, so it fails to compile with a message naming the features. A build without default features therefore has to name a release; under 0.x, a minor release.
  • A file declaring a verified release the build leaves out binds none (SpatialTree::release is None) and is classified against every release the build bundles, as an IFC4X1 or IFC4X2 file already was, rather than against an empty table that would make every entity an element.

Changed ​

  • The relationship attribute names authoring writes are constants instead of IFC4X3 table lookups, so authoring links no table; the unit test pins all 29 against the IFC4 and IFC4X3 tables.

ifc-step ​

Changed (breaking) ​

  • Requires openbim-step =0.11.0 (was =0.8.0; #288). ParseOptions and OnMalformed, re-exported here and taken by StepCodec::with_options and StepReader::new, are now that release's types, as is the source of From<openbim_step::StepError> for StepError. Code that names openbim_step 0.8 types alongside this crate must move to 0.11. The 0.9 owned-model changes (Str, Instance, boxed parameter lists) stay inside this crate: no openbim-step model type is part of its API.

Added ​

  • Lenient reading (StepCodec::lenient) accepts a REAL written without its decimal point (1E-05, -2E3, 3e+2), as buildingSMART's IFC4.x alignment test files do (#285). ISO 10303-21 requires the point (REAL = [sign] digit {digit} "." {digit} [exponent]); the meaning of such a token is not in doubt, so the read takes it as the REAL it spells and reports one located model diagnostic per token instead of skipping the record. Recognising the token is openbim-step's ParseOptions::accept_real_without_point, which ParseOptions::lenient() includes (#288); this crate only words the diagnostic. Text in strings is never touched; a token that is not a number (1E, 1EE2) is still skipped as a malformed record. The writer still always emits the point.
  • ParseOptions::strict().accept_real_without_point(true) keeps the abort policy and reads lazily: each record is validated under the read's options and its diagnostics are added in file order, exactly as the eager read adds them. ParseOptions::strict().on_malformed_record(Skip) does not accept such tokens; it skips their records, as 0.4.0 did.
  • StepError::RealWithoutDecimalPoint { offset, token }: a strict read (lazy or eager, and Index::entity) refuses such a token as before, now with an error naming it; through Codec it surfaces as ModelError::Syntax at the token's offset with the token in the detail. StepError is #[non_exhaustive], so this is not breaking.

ifc-style ​

Added (#449) ​

  • StyleIndex: every IfcStyledItem, IfcPresentationLayerAssignment and IfcMaterialDefinitionRepresentation of a model indexed once, with item_colour(item) (the item's own IfcStyledItem, then its styled presentation layers) and material_colour(material) (the item-less IfcStyledItems of the material's IfcStyledRepresentations). Both return a SurfaceColour: RGB, transparency (None when unstated), the IfcSurfaceStyle and a ColourSource (StyledItem, PresentationLayer, Material). The precedence, item over layer over material, is documented and cited (IFC4 ADD2 TC1 IfcPresentationLayerWithStyle, IfcStyledItem, IfcMaterialDefinitionRepresentation). It is applied per colour: a level that states no surface colour does not hide the next. IFC2X3 reads its IfcPresentationStyleAssignment wrappers, and its transparency from IfcSurfaceStyleRendering.
  • StyleError::AmbiguousSurfaceColour: one level stating two different surface colours (two surface styles in one styled item, two styled layers, a material whose styled representations disagree).

resolve_item_style is unchanged.

Semver: additive (StyleError is #[non_exhaustive]), a minor release while 0.x.

ifc-systems ​

Deprecated ​

  • contain_in_spatial_structure and contain_in_spatial_structure_with_owner_history in favour of ifc_spatial::contain and ifc_spatial::contain_with_owner_history, and serve_buildings and serve_buildings_with_owner_history in favour of the ifc_spatial functions of the same names (#492). Each pair wrote the same relationship with the same rules; ifc-spatial keeps the canonical writer. The deprecated functions keep their bodies (sibling domain crates do not depend on one another) and behave as before.

Semver: minor. A deprecation is a compatible change: callers get a warning, not an error.

ifc-validate ​

Added ​

  • Native WHERE rules over property sets and type assignments (#215), registered only for the releases whose EXPRESS states them and checked against it by tests/registry_scope.rs:

    • IfcObject.UniquePropertySetNames (IFC4, IFC4X3): IfcUniqueDefinitionNames(IsDefinedBy), with the inverse rebuilt from every IfcRelDefinesByProperties and an IfcPropertySetDefinitionSet opened;
    • IfcTypeObject.UniquePropertySetNames (IFC4, IFC4X3): IfcUniquePropertySetNames(HasPropertySets);
    • IfcTypeProduct.ApplicableOccurrence (IFC4, IFC4X3) and its IFC2X3 label IfcTypeProduct.WR41: every object a type product is assigned to by IfcRelDefinesByType is an IfcProduct.

    One finding per shared name; a definition the file lacks is an evaluation error. IFC2X3 states no unique-set-name rule. The textual IfcTypeObject.ApplicableOccurrence and IfcPropertySetTemplate.ApplicableEntity are stated as a rule by no release and are not checked.

  • type_check::check_value_all: every independent mismatch of one value against one declared type.

Changed ​

  • type_check::attribute_types reports every independent violation in a slot instead of the first (#215): each bad member of an aggregate, at every nesting level, and a wrapper's form together with a bad parameter inside it (IFCLABEL(12) in an IfcLabel slot is now both type.typed.outside_select and type.scalar.mismatch; a wrapper outside its SELECT is type.select.member and its parameter is still judged). Identical mismatches in one slot are reported once. A report on a malformed file can therefore hold more findings than before. type_check::check_value still returns the first.

openbim-ifc ​

Added ​

  • ifc::properties::exact_unit (ifc-properties 0.3): a measure's effective unit resolved exactly to SI, or refused (#53).
  • ifc::spatial::SpatialTree::anomalies (ifc-spatial 0.2.1): double containment and double aggregation are reported (#54).

Changed ​

  • Breaking: re-exports ifc-properties 0.3, whose UnitKind::Si::prefix_exponent is now Option<i32> and whose UnitKind::Conversion gains an offset field.

[0.4.4] - 2026-09-28 ​

ifc-geometry ​

Added ​

  • authoring::surface_curve_swept_area_solid_in and authoring::fixed_reference_swept_area_solid_in (#200). They take the model and write StartParam/EndParam in the form its declared release requires: bare in IFC2X3 and IFC4, where the slot is IfcParameterValue, and IFCPARAMETERVALUE(..) in IFC4X3, where it is the SELECT IfcCurveMeasureSelect. The release binds from FILE_SCHEMA as the other authoring crates bind it (none binds IFC4). An attribute the release requires left unset (IFC2X3 Position, StartParam, EndParam) is refused with InvalidAuthoredValue.
  • GeometryError::AuthoringSchemaUnbound (an unknown or ambiguous FILE_SCHEMA) and GeometryError::AuthoringEntityNotInSchema (the release does not declare the entity, such as the fixed-reference sweep in IFC2X3), for those writers. GeometryError is #[non_exhaustive], so this is not breaking.
  • authoring::grid_with_owner_history (#202): an IfcGrid in the model's declared release, with a caller-supplied IfcOwnerHistory, which IFC2X3 requires on every IfcRoot. It binds the release as the _in writers above do, and lays the record out by attribute name from its table, so an IFC2X3 grid has its 10 attributes, not IFC4's 11. A predefined_type in IFC2X3, which declares none, or outside the release's IfcGridTypeEnum, is refused with InvalidAuthoredValue. The owner history must be in the model or staged on the transaction and be an IfcOwnerHistory (InvalidAuthoredValue on OwnerHistory otherwise); none is ever invented. IFC4 and IFC4X3 records are grid's with the owner history in its optional slot. No error variant is added.

Fixed ​

  • IfcParameterValue slots are written bare (#200): rectangular_trimmed_surface (U1, V1, U2, V2), point_on_curve, point_on_surface, reparametrised_composite_curve_segment (ParamLength), and the StartParam/EndParam of swept_disk_solid and swept_disk_solid_polygonal. Each is declared with the defined type IfcParameterValue, not a SELECT, in every release that declares it, and ISO 10303-21 writes a typed parameter only for a SELECT. The readers accept both forms, as before.

Changed ​

  • surface_curve_swept_area_solid and fixed_reference_swept_area_solid still write IFCPARAMETERVALUE(..), which is correct in IFC4X3 only. They cannot see the release; their docs now say so and point IFC4 (and IFC2X3) callers to the _in writers.
  • authoring::grid is unchanged and documents its limitation (#202): it takes no model, so it writes the IFC4 layout (11 attributes, OwnerHistory $), which is never valid IFC2X3. It moved from authoring/transform.rs to authoring/grid.rs; the public path is the same.

[0.4.3] - 2026-09-28 ​

ifc-geometry ​

Added ​

  • BodyItem::item_world: the frame each described item is placed in, the context and product placement composed with every MappingTarget o MappingOrigin it was reached through (#185). It is reported for every item kind, including mapped B-reps and tessellations, and is not required to be rigid, so a mapping that mirrors or scales shows. BodyItem::is_mirrored() answers whether that frame reverses handedness, and Transform::determinant() gives its signed volume scale. Additive: BodyItem is #[non_exhaustive].

[0.4.2] - 2026-09-27 ​

ifc-geometry ​

Changed ​

  • data/ifc4-where-rules.tsv lists each geometry WHERE rule by entity, label and support state only. Its expression column held the rule bodies verbatim, which are CC BY-ND schema text and are no longer shipped. data/NOTICE.md covers all five data files and no longer claims the directory holds no rule bodies while it did.

[0.4.1] - 2026-10-03 ​

ifc-geometry ​

Fixed ​

  • An IfcCurveBoundedPlane lowered under a non-identity frame no longer moves its boundaries twice (#163). OuterBoundary and InnerBoundaries are in the basis plane's parameter space, which is also how the neutral curve-bounded relation reads them, but the frame was applied to them as well as to the plane: they landed elsewhere or were refused as off the plane. Only the basis plane takes the frame now, so a framed plane meshes to the identity mesh moved by the frame. Connection surfaces lowered under a space's frame are the case this breaks.

Added ​

  • product_representation_frame(model, units, product, purpose): the frame a product's representation of that purpose is placed in, the context's WorldCoordinateSystem composed above the placement chain (#164). Lowering and body_description now take their frame from it, so geometry lowered outside the body, such as a space boundary's connection surface in the relating space's coordinates, is placed exactly as the body is. Ok(None) when the product has no such representation; kernel-free.
  • profile_outline(model, units, profile) and ProfileOutline (#166): an IfcArbitraryClosedProfileDef's or IfcArbitraryProfileDefWithVoids's boundaries as rings of vertices in metres, in profile coordinates, for IfcPolyline and line-only IfcIndexedPolyCurve boundaries. Each ring is in authored order without its closing vertex, as profile lowering reads it. An IfcArcIndex segment or any other curve family is Unsupported naming the curve, never chorded; a 3D point, fewer than three distinct vertices and non-consecutive segments are Degenerate. Kernel-free.

Changed ​

  • lower_product_representation selects the representation before it resolves the placement, as body_description already did: a product with no representation of the purpose is Ok(None) even when its placement is broken, where it used to be the placement error.

ifc-properties ​

Added ​

  • exact_property, exact_properties and exact_properties_where resolve the attributes of predefined property sets (#149): IfcDoorLiningProperties, IfcDoorPanelProperties, IfcWindowLiningProperties, IfcWindowPanelProperties and every other IfcPropertySetDefinition that is neither a property set nor a quantity set, in IFC2X3, IFC4 and IFC4X3. A set's members are the attributes its entity declares below IfcPropertySetDefinition, named and typed as the declared release's table has them (LiningThickness is IFCPOSITIVELENGTHMEASURE in IFC2X3, IFCNONNEGATIVELENGTHMEASURE in IFC4). Values keep the provenance of a single value (property_id is the set's id) with no explicit unit, so a length resolves to the project unit through exact_unit; ratios are their ratio measure. New ExactValue::Enum carries an enumeration constant checked against the release's members, and ExactValue::Entity an entity reference (ShapeAspectStyle) checked but not followed. An unset optional attribute is Present with ExactValue::Null and its declared type; an unset required one is MissingValueSlot. A set that states no Name is found under its entity name (IfcDoorLiningProperties); such sets of one entity (a door's panel set per leaf) are ambiguous only for a member they share. New exact_predefined_sets(model, object, entity) returns every assigned set of that entity with all its attributes (ExactPredefinedSet, with attribute(name)), occurrence sets first, no override applied, for door operation geometry (#148); a name that is not a predefined set in the release is the new ExactPropertyError::NotAPredefinedSet. What cannot be read exactly is still refused with UnsupportedDefinition: a selected aggregate attribute (ReinforcementSectionDefinitions), and, as before, a set without Name that the set name does not select but that has an attribute of the requested name (#66). A predefined set's Name that is neither text nor $ is now MalformedName rather than treated as unnamed.

  • exact_property, exact_properties and exact_properties_where resolve IfcPropertyEnumeratedValue, IfcPropertyListValue, IfcPropertyBoundedValue, IfcPropertyTableValue and IfcPropertyReferenceValue (#150), which they refused with UnsupportedProperty. New ExactValue variants Enumerated, List, Bounded, Table and Reference carry them, with the new types ExactTypedValue (one IfcValue and its declared type), ExactEnumeratedValue/ExactEnumeration (selected values and the referenced IfcPropertyEnumeration), ExactBoundedValue (lower, upper, set point), ExactTableValue/ExactTableRow (rows, expression, defining and defined unit, interpolation), ExactReferenceValue and ExactEntityRef (usage name and target). ExactProperty.value_type is None for these; unit_id is the list's, the bounded value's or the enumeration's Unit. Every attribute is read by name from the declared release's table: IFC2X3 requires the value lists and the reference target and has no SetPointValue or CurveInterpolation; IFC4/IFC4X3 make them optional. A malformed kind is refused as a single value is, and the WHERE rules that decide how the values read (one type per list and between bounds, equal table columns, selected values drawn from the referenced enumeration) and LIST OF UNIQUE are enforced with the new ExactPropertyError::InconsistentValues { entity, rule }, where rule is the release's own label (WR21 in IFC4, WR1 in IFC2X3). Both enums are #[non_exhaustive]; callers that relied on the refusal now get answers. An IfcComplexProperty is still refused.

  • exact_properties(model, object) and exact_properties_where(model, object, select_set, select_property): exact enumeration of an object's properties and simple quantities (#78), for checks such as IDS property facets that name sets and properties by pattern. Each ExactPropertyEntry carries the member's name and the same ExactProperty (provenance, set, value type, unit, value) that exact_property reports. The traversal, model and assignment validation and refusals are those of exact_property, and for one set name and one property name the result equals its answer ([x] for Present(x), empty for Absent, the same error otherwise). An inherited property is left out when an occurrence set of the same name selects a property of the same name. An empty result is a proven absence. Only selected members must resolve: an unselected IfcPropertyEnumeratedValue or complex quantity does not refuse the answer, but every member of a selected set must be well formed. Additive; nothing existing changes.

  • exact_property, exact_schema and exact_unit resolve IFC4X3 ADD2 models (#76), bound to the bundled IFC4X3 ADD2 table. They were refused with ExactPropertyError::UnsupportedSchema. The header tokens are those ifc_schema::SchemaVersion::from_header_token maps to that release (IFC4X3_ADD2 and IFC4X3). Tests pin the IFC4X3 differences: IfcRelDefinesByProperties.RelatedObjects admits any non-type IfcObjectDefinition (an IfcProject too), IfcPropertySetDefinitionSet is traversed under its SET [1:?] rules, IfcTypeObject.HasPropertySets may be $ but not empty, IfcQuantityNumber resolves as IFCNUMERICMEASURE only in IFC4X3, and IfcDerivedUnit has IFC4X3's four attributes. IfcDimensionsForSIUnit/IfcCorrectDimensions for IFC4X3 are checked against the EXPRESS source, so the farad keeps IFC4's dimensions. Callers that relied on IFC4X3 being refused now get answers; the result type is unchanged.

  • PropertyAnomaly::MemberNotReference and PropertyAnomaly::DuplicateMember (#137). property_set_checked, property_checked, quantity_set, quantity_sets, property_sets_by_object and resolved_properties report an item of HasProperties, Quantities or HasQuantities that is not an entity reference, and a member listed twice, instead of skipping the one and reading the other twice. PropertyAnomaly is #[non_exhaustive], so this is not breaking.

  • property_checked and property_set_checked: the values of property and property_set, together with a PropertyAnomaly for every member they could not resolve (#107). property_sets_by_object and resolved_properties now include these anomalies, once per set.

  • PropertyAnomaly variants for what nested and malformed members used to lose silently (#107). PropertyAnomaly is #[non_exhaustive], so this is not breaking:

    • ComplexCycle: a complex property or complex quantity reaches itself again through its members, at any cycle length.
    • ComplexTooDeep: complex nesting deeper than 16 levels.
    • ComplexBudgetExceeded: one read followed more than 10,000 nested member references.
    • MissingMember: a set or complex entity lists an id absent from the file.
    • QuantityValueMissing / QuantityValueNotNumeric: a simple quantity whose value attribute is $, absent, or not a number.

Fixed ​

  • A member listed twice in a property set, quantity set, complex property or complex quantity was resolved twice (#137). It is now resolved once, at its first position, and reported as DuplicateMember; a property set therefore no longer reports such a repeat as a DuplicatePropertyName of itself.
  • Complex properties and complex quantities guarded nesting only against a DIRECT self-member and a bare depth of 16 (#107). A longer cycle, or nesting past the depth, was silently truncated to an empty member list, and a densely cyclic file cost exponential work (a clique of 8 complex properties followed 7^16 paths) before the depth stopped it. Members are now followed along a tracked path with a depth bound and a member budget, and every cut is reported. A member shared by two complex properties (legal: IfcProperty.PartOfComplex is SET [0:?]) is still resolved under both and is not a cycle.
  • quantity_set and quantity_sets no longer drop, without a word, a simple quantity with a missing or non-numeric value, or a member id absent from the file (#107). Such members are still left out of quantities (a Quantity::Simple needs a number), but each is now reported.
  • The property documentation claimed over-deep nesting yields PropertyValue::Unsupported; it never did. It now states what happens.

ifc-xml ​

Added ​

  • XmlCodec::xsd now writes the buildingSMART XSD configuration too (#274). Every entity is a top-level element named as the schema spells it (IfcWall) with id="i<n>"; entity values are ref references with xsi:nil, and xsi:type where the entity is a subtype of the declared type; simple values are XML attributes in their XSD lexical form (lower case enumerations, xs:double reals with a '.', xs:hexBinary), lists whitespace separated; SELECTs hold entity elements or -wrapper values; aggregates are containers of items, Seq- wrapped inner lists, or flat items with ifc:arraySize; the 21 attributes the configuration leaves off a relationship (IfcRelAggregates.RelatingObject) are written as references inside the inverse of the entity they name (IsDecomposedBy); the header is the XSD's. Each attribute's form comes from the derivation the reader uses, now one shared rule plus the configuration's few exceptions, and a schema-backed test checks every attribute, inverse and element order against both release XSDs. Every IFC4 fixture the writer accepts validates against IFC4.xsd with no error and reads back with the XSD reader to the same model (entities numbered in model order, SETs filled through inverses in document order); the STEP implementation level has no ifcXML field and is not written.
  • XmlError::Unrepresentable: the writer refuses a model the configuration cannot carry exactly rather than write a different model or an invalid document -- an integer where a real is declared, a string with a tab or line break (the XSD types strings as xs:normalizedString) or over its declared width, a string with whitespace in a list attribute, a partial-byte binary, an unset mandatory element, an aggregate outside its declared bounds, more than one header author, organization or description, IfcRelDefinesByObject.RelatingObject (the configuration writes no inverse for it), a second relationship where the XSD allows one (HasOpenings), or an inverse the entity's XSD type restricts away (IFC4 IfcOrientedEdge.StyledByItem). Models the schema does not describe are refused with the existing typed errors.

Fixed ​

  • The XSD reader accepts the namespace-qualified ifc:arraySize, ifc:itemType and ifc:cType a valid document carries: the XSD declares them as global attributes, so unqualified ones are not valid. It refused them as undeclared; unqualified ones are still read.

[0.4.0] - 2026-10-20 ​

ifc-alignment ​

Fixed ​

  • read_vertical_segment, read_cant_segment and CantLayout::resolve (RailHeadDistance) read a typed parameter such as IFCLENGTHMEASURE(1.) exactly like the bare number, as read_horizontal_segment already did; they refused it as the wrong kind (#140).

Changed (breaking) ​

  • The authoring drafts HorizontalSegmentDraft, VerticalSegmentDraft and CantSegmentDraft are #[non_exhaustive]: build them with HorizontalSegmentDraft::new(start_point, start_direction, start_radius, end_radius, segment_length, predefined_type), VerticalSegmentDraft::new(start_dist_along, horizontal_length, start_height, start_gradient, end_gradient, predefined_type) or CantSegmentDraft::new(start_dist_along, horizontal_length, start_cant_left, start_cant_right, predefined_type) and the setters gravity_center_line_height, radius_of_curvature, end_cant_left and end_cant_right. Fields stay public.
  • The read-side HorizontalSegment, CantSegment, CantLayout, CantAtStation, LinearPlacement, StationEquation, LoweredAlignmentCurve, PartialHorizontalLayout and RefusedSegment are #[non_exhaustive]; they can no longer be built with a struct literal outside the crate.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • Pinned by test: AlignmentView::for_model refuses IFC4X1 and IFC4X2 (their alignment model differs from IFC4X3).

ifc-classification ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with classification shrinks from 1,420,062 to 827,350 bytes. The default build links every release, as before.
  • A release the build leaves out is refused with UnsupportedSchema instead of panicking; so is the IFC4 baseline of a model with no header in a build without IFC4. A build without default features therefore has to name the releases it reads.

Under 0.x, a minor release.

ifc-cost ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with cost shrinks from 1,395,402 to 802,733 bytes. The default build links every release, as before.
  • A release the build leaves out is refused with UnsupportedSchema, as an unbundled release already was. A build without default features therefore has to name the releases it reads.

Under 0.x, a minor release.

Added ​

  • CostValue::applied_value(): the AppliedValue exactly as authored, typed wrapper and integer payload intact, and CostValue::unit_basis_component(model): the rate basis's ValueComponent likewise. amount() and UnitBasis::value still give the number. For lossless consumers such as the language bindings (#123).
  • Additive: a patch release.

ifc-geometry ​

Fixed ​

  • A face surface whose FaceSurface is dangling now reports the face as the referrer, and one naming a non-surface is WrongEntityType naming the target (#155). The first reported the missing id against itself; the second was an Unsupported "curved and B-spline surfaces", which reads as valid IFC this bridge declines rather than a broken reference. Applies to faces inside advanced B-reps too.

  • Compiled IfcBlock meshes were offset by half their extents. IfcBlock has a corner at its Position (IFC4 ADD2 TC1), but the neutral Primitive::Block is tessellated centred on its origin by axiolid-reference, so every compiled block sat half its size away from where the file placed it, along its own axes. Lowering now puts the half-extent shift on the block's Instance. This changes compiled geometry for every IfcBlock; the lowered Instance translation now names the block's centre. The other CSG primitives already agreed.

  • Plane, CylindricalSurface, SphericalSurface and ToroidalSurface::position(&model) now type-check their target through resource::resolve (#135). A Position naming anything other than an IfcAxis2Placement3D is WrongEntityType naming the target; it used to be wrapped as a 3D placement and misread.

  • A derived linear placement no longer reports an evaluator's degenerate curve as an undefined roll. The refusal now distinguishes an unsupported curve family, a rejected measure (off the curve, or roll undefined because the tangent is parallel to the up reference) and a degenerate curve, matching how axiolid-evaluate 0.3 reports them.

  • An opening that a file makes void two hosts is subtracted from the first only (#59). IfcFeatureElementSubtraction.VoidsElements is a single-valued inverse in IFC2X3 and IFC4, but every IfcRelVoidsElement was applied, so the second host's net body was cut by an opening that belongs to another element. The relation with the lower id now wins in openings_of and in net compilation. Output changes only for files that violate the schema.

  • IfcParameterizedProfileDef.Position now reaches the kernel for every parameterised family (#147). It was applied to rectangles and circles only, so an I, L, T, U, C or Z section, an ellipse or a trapezium with an offset or rotated Position lowered at the profile origin. Output changes only for files that author a non-identity Position on those families.

  • The translation of an IfcDerivedProfileDef operator is converted to metres (#147). It was passed through in file units, so a derived profile offset by 50 mm in a millimetre file lowered 50 m away.

  • An IfcAsymmetricIShapeProfileDef in an IFC2X3 file no longer reads CentreOfGravityInY (slot 11 in that schema) as BottomFlangeEdgeRadius (#147). IFC2X3 declares the entity as an IfcIShapeProfileDef subtype with a different tail; the declared schema now selects the layout, and the IFC4-only edge radii and slopes are absent in IFC2X3.

Added ​

  • IfcFaceSurface and IfcAdvancedFace lower as representation items (#155). Both are members of IfcSurfaceOrFaceSurface, the type of a connection surface, but the dispatcher refused them. The new lower_face_surface_node builds ONE face through the B-rep face path -- exact carrier surface, SameSense as the face's orientation relative to that carrier, each bound's Orientation -- inside one open shell with no solid, so a single face never acquires a volume. They move from the nested-only disposition ledger to IMPLEMENTED, and BodyKind::Face describes them. Committed evidence: test/fixtures/synthetic-surfaces/synthetic_space_boundary_face_surface.ifc.

  • lower_connection_surface(session, connection, frame) and lower_related_connection_surface lower an IfcConnectionSurfaceGeometry's SurfaceOnRelatingElement and optional SurfaceOnRelatedElement (#155), dispatching IfcSurface, IfcFaceSurface/IfcAdvancedFace and IfcFaceBasedSurfaceModel. Each end is authored in its own element's coordinate system, so each takes its own frame. Point, eccentric point, curve, volume and (IFC2X3) port connections are typed Unsupported refusals naming the connection; any other entity is WrongEntityType.

  • body_description(model, units, product) reports how a product's Body representation is modelled (#147). It returns one BodyItem per geometric item in authored order, with mapped items resolved (mapped_by names the chain), each carrying a BodyKind (extrusion, tapered extrusion, revolution, tapered revolution, directrix sweep, swept disk, sectioned spine, B-rep, CSG, CSG primitive, half space, bounding box, tessellated, surface model, geometric set, curve, surface, point) and, for the swept-area families, a SweptSolid: the profile description, the end profile of a tapered sweep, the solid's placement in world coordinates and a SweepPath (extrusion direction as a world unit vector plus depth in metres, revolution axis and angle in radians, or the directrix curve). Mapped geometry describes identically to the same geometry authored in place. Anything that cannot be stated exactly is a typed error for the whole body, never a partial list: unsupported item or profile families, dangling references, mapping cycles, open profiles swept as areas, and a mapping that scales or mirrors a swept solid. It is kernel-free and reachable with --no-default-features.

  • describe_profile(model, units, profile) reads any concrete IfcProfileDef into a ProfileDescription: type, ProfileName, Position, and ProfileParameters for every family in metres and radians (rectangle, rounded and hollow rectangle, circle and hollow circle, ellipse, I, asymmetric I, L, T, U, C, Z, trapezium, arbitrary closed, with voids and open by curve reference, centre line, composite, derived with its operator, mirrored). A bare IfcProfileDef and unknown families are refused; a composite or derived chain that references itself is CyclicChain.

  • derive_placement_transform derives a linear placement on an IfcPolyline or a line-only IfcIndexedPolyCurve basis curve (#96), not only on an alignment. The curve lowers to the neutral polyline, whose arc length is an exact finite sum, so a distance converts to a parameter exactly; a native parameter follows the IFC polyline parameterisation (one per segment). Refused by name: a zero-length segment, fewer than two points, non-consecutive Segments, an IfcArcIndex, a parameter on a multi-point IfcLineIndex (IFC does not state its split), and ellipse and B-spline bases as before.

  • product_bounds bounds linear extrusions of straight-edged profiles (rectangles, polyline contours, and placed/derived forms of them) and blocks exactly from their vertices, without tessellating (#98). The result reports BoundsSource::Exact. Curved profiles, rounded rectangles and booleans still go through the compiled mesh; a difference only shrinks its operand, so its operand's box is never used.

  • voiding_conflicts(model) and VoidingConflict { opening, kept_host, rejected_host, relation } report such openings (#59). It is kernel-free, like openings_of. Restating the same host is not a conflict.

  • Resolving typed accessors beside the raw *_ref getters (#97), following Plane::position(&model): Line::point, Polyline::points, IndexedPolyCurve::points, BSplineCurve::control_points, OffsetCurve3D::ref_direction, Circle::position, Ellipse::position, Trim::cartesian_point, BSplineSurface::control_point_views, SurfaceOfLinearExtrusion::{position, extruded_direction}, SurfaceOfRevolution::{position, axis_position}, BoundingBox::corner_point and TessellatedFaceSet::coordinate_list. Each type-checks its target: a dangling reference is MissingEntity naming the referrer, a wrong type is WrongEntityType naming the target. All are kernel-free.

  • resource::resolve, the shared type-checked resolvers behind them, and two select views: Axis2Placement (2D or 3D, for IfcConic.Position) and CartesianPointList (2D or 3D, for IfcIndexedPolyCurve.Points). Profiles stay references; describe_profile owns IfcProfileDef reading.

  • compile::product_bounds / product_bounds_with return a product's world-space axis-aligned bounding box (#36). The body is resolved and placed the same way as for compile_product_mesh, and the result is Ok(None) in the same case (no body).

    • When every leaf of the lowered graph is a mesh or an authored bounding box, the box is read off the exact graph without tessellating (BoundsSource::Exact).
    • Otherwise it comes from the compiled mesh (BoundsSource::Tessellated). That box is exact for planar geometry and can fall short of a curved surface by up to the tolerance.
    • A body with no finite extent is GeometryError::Degenerate, never an empty box.
  • examples/product_bvh.rs indexes every product of a file in axiolid_spatial::Bvh and prints the broad-phase overlaps. axiolid-spatial is a dev-dependency only: the index stays Axiolid's, and this crate only produces the boxes.

Changed ​

  • lower::profile builds every profile from describe_profile instead of reading slots itself (#147), so lowering and body description cannot disagree about a slot, a unit or a default. A profile nesting chain that exceeds its budget is now ChainTooDeep rather than Unsupported, and a self-referencing chain is CyclicChain; a dangling boundary curve of an arbitrary profile is reported when the profile is read.
  • Requires axiolid-mesh-compile 0.3.4, axiolid-construct 0.3.3 and axiolid-evaluate 0.3.1. Compiled output changes where the kernel's did: a B-rep's void shells are tessellated facing into the cavity instead of being dropped, so an authored cavity is no longer filled (axiolid/kernel#120); every solid of a multi-solid B-rep is meshed, not only the first (axiolid/kernel#111); and a curve-bounded plane, the usual space-boundary connection surface, compiles to a planar surface mesh instead of being refused (axiolid/kernel#192).
  • Requires axiolid-mesh-compile 0.3.3 and axiolid-contracts 0.3.1. Closed IfcPolygonalFaceSet bodies whose face corners lie on a straight run (collinear notch and window heads) now mesh closed and report Solid (axiolid/kernel#170); before, the triangulation left T-junction cracks and they came back Surface. Surface models with a zero-area bowtie face, as Nova MEP exports write pipe-fitting end caps, now compile instead of being refused (axiolid/kernel#171). On 12 real models (66,659 products) this moves 1,999 products to Solid and failures from 2,291 to 755, together with the kernel#168 and #169 fixes already required.
  • tests/meshing_coverage.rs pins both: a real ArchiCAD lining at its exact coordinates meshes to its divergence volume as a solid, and a surface model with a bowtie cap keeps its area. A third test checks that an explicit chord budget (ExecutionOptions::with_chord_error, axiolid/kernel#165) brings the composite-curve D within 1e-5 of its exact volume.

ifc-georef ​

Added ​

  • ProjectedCrs.well_known_text: the OGC WKT literal of the one IFC4X3 IfcWellKnownText defining the CRS, verbatim (#142).
  • GeorefError::RuleViolation { entity, rule } for a schema WHERE rule or inverse cardinality a record breaks.

Changed (breaking) ​

  • ProjectedCrs.name is Option<String> (#142). IFC4X3 declares IfcCoordinateReferenceSystem.Name : OPTIONAL IfcLabel with WHERE NameOrWKT : (HIINDEX(WellKnownText) = 1) OR EXISTS(Name): under an IFC4X3 header an unnamed CRS defined by exactly one IfcWellKnownText now reads, one with neither is refused with the new GeorefError::RuleViolation { rule: "NameOrWKT" }, and two definitions for one CRS (the inverse is SET [0:1]) are refused too. IFC4, or a missing or ambiguous header, keeps requiring the name (MissingAttribute), so name is always Some there. Migrate with crs.name.as_deref().
  • The authoring drafts ProjectedCrsDraft, GeographicCrsDraft and MapConversionDraft are #[non_exhaustive]: build them with ProjectedCrsDraft::new(name), GeographicCrsDraft::new() or MapConversionDraft::new(source_crs, target_crs, eastings, northings, orthogonal_height) and a setter named after each optional field (.map_unit(unit), .x_axis((abscissa, ordinate)), .scale(s)). Fields stay public.
  • The read-side ProjectToMap and ProjectedCrs are #[non_exhaustive]; they can no longer be built with a struct literal outside the crate.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-material ​

Changed (breaking) ​

  • MaterialView::profile_set_usages() also yields IfcMaterialProfileSetUsageTapering, the schema subtype of IfcMaterialProfileSetUsage in IFC4 and IFC4X3, in entity-id order, and MaterialProfileSetUsage::try_new / try_from_view accept it (#136). A caller that iterated both profile_set_usages() and tapering_profile_set_usages() sees each tapering usage twice; iterate profile_set_usages() alone and branch on the new MaterialProfileSetUsage::tapering(), which returns the tapering projection (end_profile_set_id, cardinal_end_point) or None.
  • The authoring drafts MaterialDraft, LayerDraft, LayerSetDraft, MaterialAssignmentDraft, ConstituentDraft and ProfileDraft are #[non_exhaustive]: build them with MaterialDraft::new(name), LayerDraft::new(thickness), LayerSetDraft::new(layers), MaterialAssignmentDraft::new(global_id, related_objects, relating_material), ConstituentDraft::new(material) or ProfileDraft::new(profile) and a setter named after each optional field (LayerDraft::new(0.2).material(brick).priority(80)). Fields stay public.
  • ResolvedAssignment is #[non_exhaustive].

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • UnsupportedSchema reads "a release this crate has no verified layout for" instead of "no bundled schema table".
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-properties ​

Added ​

  • exact_unit maps IFCSECTIONALAREAINTEGRALMEASURE to SECTIONAREAINTEGRALUNIT, whose name differs from the measure's. Before, it was refused as unmapped. The pairing follows the measure's definition (m^5) and the IFC4 annex E structural example.
  • The permissive views report the duplicates they resolve (#58). exact_property already refuses these files; the permissive views now keep a documented winner and say so:
    • PropertyAnomaly::TypedTwice: an object with two IfcRelDefinesByType (IFC4 IsTypedBy is SET [0:1]; IFC2X3 IfcObject WR1).
    • PropertyAnomaly::DuplicateSetName: two same-named property sets on one occurrence or one type (IFC4 UniquePropertySetNames). It is reported once per owner, including for a type that no occurrence uses.
    • PropertyAnomaly::DuplicatePropertyName: two same-named properties in one set (IFC4 UniquePropertyNames, IFC2X3 WR32), reported by property_sets_by_object.

Changed (breaking) ​

  • PropertyAnomaly is #[non_exhaustive], so future checks can add variants. Exhaustive matches need a wildcard arm.
  • template_of_set returns BTreeMap<EntityId, Vec<EntityId>>: every template defining a set, ascending by id and without repeats (#60). IfcPropertySetDefinition.IsDefinedBy is SET [0:?] OF IfcRelDefinesByTemplate, so several templates are legal, but the old BTreeMap<EntityId, EntityId> kept only the last and silently dropped the others, on valid files. A caller that wants one template must now choose, and is not handed an arbitrary one.

Fixed ​

  • An object typed twice kept the last IfcRelDefinesByType in file order and said nothing. It now keeps the first by relationship id, and inherits only that type's sets.
  • Two same-named property sets on one owner made the later one win. For occurrences, the earlier one was misreported as shadowed: shadowing means an occurrence set overriding a type set. The set with the lower id now wins within each route. An occurrence set still overrides a same-named type set, and that is not reported.

Fixed ​

  • exact_property no longer reports Absent for a quantity in a same-named IfcElementQuantity (#66). Quantity sets were skipped, so a checker asking for Qto_WallBaseQuantities.Length (IDS treats quantities as properties) got a confident "missing".
    • Simple quantities now resolve exactly. value_type is the release's declared measure of the value attribute (IFCLENGTHMEASURE), and unit_id is the quantity's IfcNamedUnit.
    • Complex, duplicated or malformed quantities are refused, as are a same-named property set and quantity set.
    • A predefined property set (IfcDoorLiningProperties, …) whose own attribute carries the requested name is refused with UnsupportedDefinition instead of being skipped into Absent.
    • A quantity set with no Name is refused (MalformedName), because it could be the set asked for.
    • Results change from Absent to Present or an error only where the old answer was unproven.

ifc-resource ​

Added ​

  • IFC2X3 TC1 resource profile (#237). ResourceView::for_model and ResourceView::new accept an IFC2X3 model and read it through IFC2X3's own bundled table, by attribute name. Shared concepts answer the shared accessors: ConstructionResource::identification reads ResourceIdentifier, Person::identification and Organization::identification read Id, Inventory::predefined_type reads the required InventoryType. IFC2X3-only accessors: ConstructionResource::resource_group, resource_consumption, base_quantity_measure, skill_set, suppliers, usage_ratio, sub_contractor, job_description, and Inventory::last_update_calendar_date. New MeasureWithUnit projection (ResourceView::measure_with_unit) for the IFC2X3 BaseQuantity. IFC2X3's IfcPerson.WR1 and the material/product resource WR1/WR2 are enforced.
  • ResourceError::NotInSchema: the view's release does not declare the entity or attribute an accessor reads, in the form it reads it. IFC2X3 resource types, IfcResourceTime, LongDescription, Usage, BaseCosts, PredefinedType, Formula, the IFC4 BaseQuantity and LastUpdateDate forms, and the IFC2X3-only accessors under IFC4/IFC4X3 refuse with it.

Changed ​

  • Behaviour change: an IFC2X3 model is no longer refused by ResourceView with UnsupportedSchema; it is read as above. ResourceEditor::for_model still refuses it with UnsupportedSchema (authoring stays IFC4/IFC4X3), and nothing is staged.
  • Reading an attribute the bound schema does not declare on the entity's type is now NotInSchema instead of MissingAttribute; MissingAttribute is kept for a stored record shorter than its declared layout. No IFC4/IFC4X3 accessor reads an undeclared attribute, so this is reachable only through the IFC2X3 profile.
  • The crate status is implemented; no gap remains open.

ifc-schedule ​

Added ​

  • read_work_calendars and read_events bind the model's declared release like tasks (#212) and return Result<_, ScheduleReadError>: IFC4X1, IFC4X2 and several schemas are refused with UnsupportedSchema or MultipleSchemas; IFC2X3, which declares neither entity, has none (#234). WorkCalendar and Event expose release() and global_id(), Event also object_type() and user_defined_trigger_type() (UserDefinedEventTriggerType); WorkTime and EventTime carry data_origin and user_defined_data_origin, and EventTime its name (#234).
  • TaskTime::is_recurring, TaskTime::recurrence_ref and TaskTime::recurrence expose an IfcTaskTimeRecurring's pattern (#235).
  • Task::schedule_time_controls and the ScheduleTimeControl view read the IFC2X3 IfcScheduleTimeControl that IfcRelAssignsTasks.TimeForTask assigns to a task, every attribute by name from IFC2X3 TC1: dates as AuthoredDateTime records, durations and floats as IfcTimeMeasure numbers. A TimeForTask of another type is reported as the new TaskTimeAnomaly::NotAScheduleTimeControl (#235).
  • RecurrenceDraft::time_periods places IfcTimePeriod records in a pattern's TimePeriods (#233). create_recurrence_pattern_in and create_time_period_in bind the model's declared release: IFC2X3, which declares neither entity, is refused with EntityNotInSchema, and the pattern writer refuses a period reference that is absent (MissingReference) or not an IfcTimePeriod (WrongReferenceType). Nothing is staged on a refusal.
  • Recurrence exposes days (DayComponent), months (MonthComponent) and time_periods, read as the new TimePeriod (StartTime, EndTime as authored); recurrence_pattern(model, id) reads a pattern referenced from outside a work calendar, such as IfcTaskTimeRecurring.Recurrence, by name in the declared release, returning Result<Option<Recurrence>, ScheduleReadError> (#234); recurrence_slot::TIME_PERIODS and time_period_slot name the slots (#233).
  • Lag exposes duration_type (IfcLagTime.DurationType) and name (#236).
  • process_execution_order: every IfcProcess (task, procedure, event) in a deterministic execution order (#236).
  • ScheduleReadError::SequenceDepthExceeded { start, limit } (#236).

Deprecated ​

  • work_calendars and events: use read_work_calendars and read_events. They keep their signatures and now read by name in the declared release too, but read a header the readers cannot bind as IFC4, as before, instead of refusing it (#234).

Fixed ​

  • The calendar, event and recurrence readers find every attribute by name in the declared release instead of reading fixed IFC4 positions; IFC4X3's IfcWorkTime.StartDate and FinishDate were read only because they sit where IFC4's Start and Finish do (#234).

  • Task::time accepts an IfcTaskTimeRecurring (IFC4, IFC4X3) through the release's subtype table; it was reported as NotATaskTime and dropped (#235).

  • create_task_time_recurring binds the model's declared release: IFC2X3, which declares no IfcTaskTimeRecurring, is refused with EntityNotInSchema, nothing staged (#235).

  • tasks_of_schedule reads every IfcRelAssignsToControl subtype in the bound release, so an IFC2X3 task assigned with IfcRelAssignsTasks is listed (#235).

  • create_recurrence_pattern writes all eight attributes IFC4 and IFC4X3 declare for IfcRecurrencePattern; it wrote seven, so every authored pattern was a short record (#233).

  • A sequence walk that reaches MAX_SEQUENCE_DEPTH is refused with SequenceDepthExceeded instead of returning a truncated result as if complete: downstream_of and find_cycle report it (#236). The walk uses an explicit stack, so the budget rather than the thread's stack bounds it.

  • find_cycle walks from every IfcProcess in the declared release, not only IfcTask, so a cycle through events or procedures is found (#236).

  • execution_order sorts over every IfcProcess and keeps the tasks, so a constraint through an event or procedure (task A, event E, task B) orders A before B, and a cycle through one is refused (#236). Its result is still tasks only.

ifc-spatial ​

Changed (breaking) ​

  • serve_buildings takes the &Model, binds its declared release and checks its targets (#286), as ifc_systems::serve_buildings does (#277). It accepted any targets: per the EXPRESS sources RelatingSystem must be an IfcSystem and RelatedBuildings a SET [1:?] OF IfcSpatialStructureElement (IFC2X3 TC1) or OF IfcSpatialElement (IFC4 ADD2 TC1, IFC4X3 ADD2). Without the model it could not see either end. Like the other release-bound plain writers it refuses IFC2X3, which requires OwnerHistory, with AuthoringRequired; use serve_buildings_with_owner_history there. IFC4 and IFC4X3 records are unchanged, slot for slot.
  • Behaviour change: serve_buildings and serve_buildings_with_owner_history now refuse, staging nothing, input they used to write: a system that is missing (MissingReference) or not an IfcSystem (WrongReferenceType), a building that is missing or not admitted by the declared release's RelatedBuildings (WrongReferenceType; an IfcSpatialZone is admitted in IFC4 and IFC4X3, not in IFC2X3), a repeated building (Invalid, the attribute is a SET), and a second relationship for a system that already services buildings in the model or on the transaction (Invalid; IfcSystem.ServicesBuildings is SET [0:1]). The writers moved to authoring::services; the public paths are unchanged.

ifc-structural ​

Changed (breaking) ​

  • LoadKind is #[non_exhaustive] and gains SingleForceWarping, SingleDisplacement and SingleDisplacementDistortion (#228). An exhaustive match on it no longer compiles; add a wildcard arm.

Added ​

  • IFC2X3 varying linear and planar actions (#229). StructuralAction::is_varying, varying_applied_load_location and subsequent_applied_loads read IfcStructuralLinearActionVarying and IfcStructuralPlanarActionVarying; the loads come back in list order as StaticLoad, the LIST [1:?] / LIST [2:?] minimums are enforced with InvalidCardinality, and all three return None/false under IFC4 and IFC4X3, which declare neither entity. ActionDraft::varying with the new VaryingActionDraft stages either subtype from a Linear or Planar kind; outside IFC2X3 it refuses with the new StructuralError::EntityNotInSchema, and a short list, another kind or a wrong reference refuses before anything is staged.
  • StructuralView::surface_reinforcement_area and the SurfaceReinforcementArea projection for IfcSurfaceReinforcementArea (IFC4, IFC4X3), which stage_load already authored (#228). It enforces SurfaceAndOrShearAreaSpecified, NonnegativeArea1..3 and the LIST [2:3] bounds; an IFC2X3 view refuses it with UnsupportedSchema.

Fixed ​

  • StructuralView::load / static_load read IfcStructuralLoadSingleDisplacement, IfcStructuralLoadSingleDisplacementDistortion and IfcStructuralLoadSingleForceWarping instead of refusing them with WrongType (#228). Classification uses Schema::is_a, most specific subtype first, and components() returns the displacement and rotation slots, with Distortion or WarpingMoment appended for the subtypes.

ifc-style ​

Changed (breaking) ​

  • AppearanceSupport is #[non_exhaustive]: a match needs a wildcard arm; AppearanceDeclaration is #[non_exhaustive].
  • Every authoring draft is #[non_exhaustive], so a struct literal no longer compiles outside the crate. Each gains new(required…) and one builder setter per other field, named after the field and taking the unwrapped value: BlobTextureDraft::new(raster_format, raster_code), TextModelDraft::new(), LightSourceDraft::new(light_colour), AnnotationDraft::new(global_id), TextLiteralDraft::new(literal, placement, path), TextLiteralWithExtentDraft::new(literal, placement, path, extent, box_alignment), AnnotationFillAreaDraft::new(outer_boundary), CurveStyleDraft::new(), PixelTextureDraft::new(width, height, colour_components, pixel), ColourRgbDraft::new(red, green, blue), SurfaceStyleShadingDraft::new(surface_colour), SurfaceStyleDraft::new(side, elements), StyledItemDraft::new(styles), PresentationLayerDraft::new(name, assigned_items), SurfaceStyleRenderingDraft::new(surface_colour, reflectance_method) and ImageTextureDraft::new(url_reference). Fields stay public.

Added ​

  • StyleError::UnsupportedSchema: StyledItem::styles refuses a recognised release it is not verified for instead of applying the IFC4X3 rule to it.

Changed ​

  • Links no bundled schema table itself: every entry point takes the Schema from the caller. A consumer that used a table through this crate's dependency (ifc_schema::ifc4()) enables it on its own ifc-schema dependency (default features bundle every release).
  • StyledItem::styles reads IFC4X1 and IFC4X2 like IFC4: both declare Styles over IfcStyleAssignmentSelect, which still admits IfcPresentationStyleAssignment; only IFC4X3 narrowed it. Pinned against both bundled tables.

ifc-systems ​

Changed (breaking) ​

  • Release features ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all default, forward to ifc-schema, which this crate now depends on without its default features (#306). Each links one release's table, so a single-release build carries only that table: an IFC4-only browser module with systems shrinks from 1,423,611 to 831,006 bytes. The default build links every release, as before.
  • Authoring refuses a release the build leaves out with SystemAuthoringError::UnsupportedSchema instead of panicking; reads already refused it. A build without default features therefore has to name the releases it reads.

Under 0.x, a minor release.

ifc-template-catalog ​

Changed (#317) ​

  • The embedded snapshots are one container, data/catalog.bin (snapshot format 3), instead of three bincode files: 1,423,604 bytes instead of 3,648,573 (547,657 instead of 966,507 under gzip -9). Strings used more than once are stored once in a table, strings used once stay inline, and each distinct property, quantity and set record is stored once across editions; each edition keeps its manifest, source digest and per-template provenance. Lossless: every edition decodes, in the official and the corrected profile, to exactly the catalogs format 2 did, which a test asserted against the old files before they were removed; content fingerprints taken from them stay pinned in tests/snapshot.rs.
  • The first lookup of an edition decodes that edition only.
  • The generator (ifc-template-catalog-generate) writes one edition into the container and keeps the others; the hidden generation module and the format-2 codec are gone.

Added (#317, #318) ​

  • Module snapshot: encode (a container or a per-edition file, deterministic), decode_edition, decode_all, editions, file_name, the pins CONTAINER_SHA256 and pinned_sha256, EDITIONS, FORMAT_VERSION and EncodeError. ArchiveError is public there and gains MissingEdition.
  • Feature runtime, module runtime: install an edition from its per-edition file or the container, checked against the pinned SHA-256 (RuntimeCatalogError::DigestMismatch otherwise), then load_catalog in the official or corrected profile, is_installed and decode_verified; an edition not installed is RuntimeCatalogError::NotInstalled. Builds for wasm32; embedded stays the default.
  • Example export_snapshots writes the per-edition files and checks each pin; the npm package ships them.

Semver ​

  • A new public module, feature and error variant, and a changed data layout under data/: a minor release (0.4.0). The API callers used (embedded::*, Catalog, the definitions) is unchanged.

ifc-validate ​

Added ​

  • Aggregate checks from the schema's bounds (#111): a level outside its declared size (structure.aggregate.too_few, structure.aggregate.too_many; ARRAY [l:u] needs exactly u-l+1), an inner level of a nested aggregate that is not an aggregate (structure.aggregate.nesting), and a repeated element in a SET or UNIQUE level (structure.aggregate.duplicate). The members of a LIST OF LIST are now type-checked against the innermost element type (#215).
  • Every UNIQUE clause of the declared release is checked across the declaring entity and its subtypes (structure.unique.violation), except IfcRoot.UR1, which stays global.UniqueGlobalId.

Changed (breaking) ​

  • structure::duplicate_global_ids and its rule id structure.unique.duplicate_global_id are removed: the function duplicated global.UniqueGlobalId and validate never ran it. Its module now checks the release's UNIQUE clauses (structure::unique_rules, run by validate).
  • No registered rule claims to need aggregate bounds any more: IfcPolyLoop.WR21 and IfcPolyLoop.AllPointsSameDim are unsupported for needing an expression evaluator.
  • Support is #[non_exhaustive]: a match needs a wildcard arm.
  • Finding is #[non_exhaustive]; it can no longer be built with a struct literal outside the crate.
  • Path is #[non_exhaustive], so a later release can name a new location kind; a match needs a wildcard arm.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • validate_declared validates IFC4X1 and IFC4X2 files against their own bundled tables instead of refusing them as unknown. No WHERE rule is registered for either release yet, so their report carries one where.release finding (severity Unsupported) saying WHERE rules were not evaluated, rather than reading as if they passed.
  • SELECT resolution treats a type declaration form ifc-schema adds later like an undeclared member (fails closed); follows ifc_schema::TypeKind becoming #[non_exhaustive].

ifc-xml ​

Changed (breaking) ​

  • A codec built with XmlCodec::with_schema or XmlCodec::with_schema_and_profile now reads strictly (SchemaReading::Strict, #266). Every value is typed from its attribute's declaration instead of inferred from its text: Name="1" reads as the label '1' (it read as the integer 1), an enumeration's text is checked against its members, a kind the declaration does not admit (an integer in a label, a bare value in a SELECT, a typed value outside it) is refused. An XML attribute or child element the entity does not declare is refused with XmlError::UnknownAttribute, naming entity, element and attribute; it was placed after the declared slots. An entity the schema does not declare, an abstract one, and a reference to a missing entity or one of a type the declaration does not admit are refused too. The old read silently misread such content, so the safe read is now the default; .with_reading(SchemaReading::Lenient) restores it, and the codec without a schema is unchanged. Unknown entities and attributes still round-trip through the schema-less codec and the lenient read.
  • The schema-aware writer writes a value as an XML attribute only where the strict read types its text back to the same value; elsewhere it uses the explicit kind element, which the lenient read accepts and the strict read refuses unless the declaration admits it. Output for models the schema describes is unchanged.

Added ​

  • XmlCodec::xsd and XmlLayout::Xsd: a reader of the buildingSMART ifcXML configuration the release XSD declares (#265), for IFC4 ADD2 TC1 and IFC4X3 ADD2. Entities nested and defined in place, ref/href references with xsi:nil, xsi:type, inverse attributes (which supply the attribute they invert), space-separated and flattened list attributes, -wrapper and Seq-...-wrapper values, and enumeration, boolean, logical and binary text typed from the schema, read into the same model as the STEP form: entity type, typed-parameter and enumeration names upper-case, entities numbered in document order. Refused with a typed error rather than approximated: a name the entity does not declare, a value its declaration does not admit (including decimal commas), a flattened nested aggregate whose inner sizes neither the schema nor an arraySize fixes, an inverse contradicting its relationship or implying a position in an ordered list, pos/path addressing, external hrefs, partial-byte binaries. A value for a slot a subtype redeclares DERIVE, which the XSD still admits as an XML attribute, is typed and read as derived. The layout is read only; writing it is refused.
  • XmlProfile::Ifc4x3Add2, XmlProfile::namespaces (IFC4 ADD2 TC1 also accepts the http://www.buildingsmart-tech.org/ifcXML/IFC4/Add2 namespace its own Annex E examples declare, in the XSD layout only) and XmlProfile::version.
  • SchemaReading, XmlCodec::with_reading, XmlCodec::reading and XmlCodec::layout.
  • XmlError variants UnknownEntity, AbstractEntity, UnknownAttribute, TypeMismatch, WrongForm, UnresolvedReference, DuplicateId, InverseConflict, SchemaMismatch and Unsupported.
  • XmlCodec implements Debug and Clone; XmlError implements Clone.
  • A schema-backed test checks the XSD reader's configuration against every entity type of both release XSDs, which scripts/fetch-ifc-schemas.sh now fetches (checksummed, never committed). An env-gated test compares paired STEP and ifcXML files (tests/xsd_corpus.rs documents how to run it).
  • tests/xsd_output.rs, an opt-in (--ignored) check that writes every IFC4 and IFC4X3 fixture with the strict profile and validates it with xmllint against the fetched release XSD (#117). It fails on malformed output, a namespace that is not the XSD's targetNamespace, a control document the XSD or XmlCodec::xsd refuses, and any XSD error beyond the documented native-layout departures. The published IFC4X3_ADD2.xsd does not compile in libxml2 or Xerces, which the check records. How to run it is in the README.

Security ​

  • Require quick-xml 0.42 (was 0.37), which fixes RUSTSEC-2026-0194 (quadratic duplicate-attribute check on one start tag) and RUSTSEC-2026-0195 (unbounded namespace-declaration allocation in NsReader); both are denial of service on untrusted input (#267).

Changed ​

  • impl From<quick_xml::Error> for XmlError is kept, but its source type is now quick-xml 0.42's Error. Code that converts a quick-xml 0.37 error into XmlError must upgrade quick-xml too. Values read are unchanged: entity and character references are still resolved, and literal tabs and line breaks in attribute values and text are still kept rather than normalised to spaces; a regression test pins this.

Documentation ​

  • Strict-profile output is documented as what it is: the crate's own layout under the release XSD's target namespace and schema token, not the XSD configuration, and not valid against the release XSD (#117). XmlProfile no longer calls the XSD "bundled"; it is fetched, never shipped.

openbim-ifc ​

Added ​

  • Textured tessellated geometry: ifc::geometry (ifc-geometry 0.3) lowers IfcIndexedTriangleTextureMap as a per-corner uv channel that survives compilation, including multi-item bodies and mirrored IfcMappedItems (#30, axiolid/kernel#115).

Changed ​

  • Breaking: re-exports ifc-geometry 0.3, ifc-georef 0.3 and ifc-alignment 0.3, which all require Axiolid 0.3.

openbim-ifc-cli ​

Added (#469) ​

  • convert-units INPUT OUTPUT --unit UNIT [--keep-area-volume] [--keep-replaced-units] [--dry-run] [--format human|json] [--force]: the facade's convert_length_unit on a file, written as STEP. A value the conversion cannot carry refuses the run (exit 2) with each one named, and nothing is written.

Semver: minor (a new command).

Added (#472, #474) ​

  • edit FILE QUERY --set PATH VALUE... (-o FILE | --in-place | --dry-run) writes values by path on the entities a selector selects, as one checked transaction, each value read as its target's type; --dry-run lists the changes and writes nothing; the input is never changed without --in-place, which keeps its format; --null names the text of $; --format json reports changes and refusals as one document.
  • export-table FILE QUERY [-c PATH]... [-o FILE] [--to csv|tsv|xlsx] [--key global-id|id] [--null TEXT] writes the selected entities as a table, one column per value path.
  • import-table FILE TABLE (-o FILE | --in-place | --dry-run) [--from csv|tsv|xlsx] [--key ...] [--null TEXT] applies a table back by key as one checked transaction; an unchanged table writes an identical model.
  • Exit code 3: edit or import-table refused a planned change; every refusal is printed with its entity and path or its row and column and a kind (invalid-text, type-mismatch, not-settable, conflicting-write, property-edit, invalid-header with its span, unknown-key, ragged-row, ...), and nothing is written.
  • relations FILE ENTITY [--traverse-up] lists what one entity is related to by kind (container, parent, type or occurrences, material and materials, classification, groups, openings, fillings, voided element) and every relationship naming it; --traverse-up the chain of parents.
  • materials FILE [QUERY] shows each selected entity's material definition with its layers, profiles, constituents or list members.
  • cost FILE [--depth N] shows every cost schedule as a tree of cost items with their values, direct total and rolled-up total; schedule FILE [--depth N] every work schedule and work plan as a tree of tasks with their scheduled times. All four write --format human or json.
  • The command now checks Pset_ and Qto_ writes against the embedded PSD/QTO catalog (facade feature property-catalog), which grows the binary.

Semver: minor (additive; exit codes 0, 1 and 2 keep their meaning).

[0.3.3] - 2026-10-20 ​

ifc-schema ​

Added (#467) ​

Semver: patch (0.x: a new public type only, nothing changed). SchemaVersion and its header-token mapping are unchanged.

  • UnpublishedRelease (Ifc4x3Tc1, Ifc4x3Add1) names the IFC 4.3 releases whose identifiers files carry but whose normative EXPRESS buildingSMART does not publish: IFC4X3_TC1 (IFC 4.3.0.1) and IFC4X3_ADD1 (IFC 4.3.1.0), each with identifier(), version_number(), superseded_by() (IFC4X3 ADD2, not a claim of identity) and from_header_token(). The identifiers and numbers come from the SCHEMA_NAME and documentation header of buildingSMART/IFC4.x-development's generator; buildingSMART lists only ADD2 (4.3.2.0) for IFC 4.3, so no tables can be fetched and checksummed for TC1 or ADD1, and IfcOpenShell's own (non-normative) tables show TC1 lacks entities ADD2 has. SchemaVersion::from_header_token still returns None for both: they are named for a precise refusal, never read as ADD2 by assumption.

[0.3.2] - 2026-10-14 ​

ifc-alignment ​

Fixed ​

  • Referent authoring writes the SELECT values typed (#201):

    • point_by_distance writes DistanceAlong as IFCLENGTHMEASURE(..). It is an IfcCurveMeasureSelect, where the wrapper is what tells a length from a curve parameter. The offsets stay bare.
    • stationing writes Pset_Stationing's NominalValues, declared IfcValue, as IFCLENGTHMEASURE(..) for Station and IncomingStation and IFCBOOLEAN(..) for HasIncreasingStation.

    station_equations reads both forms, as before.

ifc-author ​

Fixed (#481) ​

  • EntityBuilder checks a nested aggregate (LIST [1:?] OF LIST [3:3] OF IfcLengthMeasure, as IfcCartesianPointList3D.CoordList and IfcTriangulatedFaceSet.CoordIndex declare) at its elements, level by level, instead of refusing the inner lists as values of the element type; a level that should hold aggregates and holds a scalar is AggregateMismatch.

Semver: patch.

ifc-schema ​

Changed ​

  • Schema::is_a, supertypes and attributes read a supertype chain and a positional attribute layout precomputed per entity when the schema is assembled, instead of walking the declarations on every call (#352). is_a no longer allocates, and no case-folded lookup (entity, type_def, direct_subtypes) allocates for a name of up to 128 bytes. The answers are unchanged: the tables are built by the walks they replace, and tests/precomputed_lineage.rs compares every entity of every bundled release, under three spellings, against a frozen copy of the old walks, and is_a for every ordered pair.
  • accepts_type answers two distinct declared entities with is_a directly, without the select walk's allocations; the answer is the one the walk gave.

Added ​

  • Schema::attribute_count(name) and Schema::attribute_at(name, slot): attributes(name).len() and attributes(name).get(slot) without allocating (#352).

Semver: additive, same answers: a patch release.

[0.3.1] - 2026-10-04 ​

ifc-alignment ​

Changed ​

  • profile_law (and so lower_gradient_curve) now refuses a vertical profile whose seams do not join: a segment's StartHeight must match the previous segment's end height and its StartGradient the previous EndGradient, within the same magnitude-scaled tolerance already used for StartDistAlong contiguity. A height step or grade kink was previously accepted and silently shifted every downstream height. The refusal is the new AlignmentError::ProfileDiscontinuity, naming both segments, the discontinuous quantity (ProfileSeam::Height or ProfileSeam::Gradient) and both values (#95).

ifc-author ​

Fixed (#330) ​

  • A built-in type with a width (STRING(255), STRING(22) FIXED, BINARY(32)) is the shape of its keyword. IFC4 and IFC4X3 declare IfcLabel = STRING(255), so a number set as an IfcLabel was accepted as unresolvable; it is now refused with TypeMismatch, as a number where a bare STRING is declared always was. A bug fix: a patch release.

ifc-control ​

Added ​

  • Borrowed read views for the four controls (#100): read_control and read_controls return a Control with GlobalId, OwnerHistory, Name, Description, ObjectType, Identification (IFC2X3 PermitID, RequestID, ID), PredefinedType, Status, LongDescription and LifeCyclePhase, each found by name in the model's declared release, plus Control::declares and Control::assignments (ControlAssignment views of the IfcRelAssignsToControl the crate writes). A record that does not fit its release is refused with the new ControlError::InvalidAttribute or ControlError::ExtraAttributes, or with MissingAttribute; a foreign type with ForeignControl.
  • ControlKind::from_type_name.

ifc-geometry ​

Added ​

  • compile::compile_product_mesh_reported (and _with) return a CompiledMesh: the triangles plus the kernel's MeshClosure, i.e. whether they bound a solid. CompiledMesh::solid_mesh(product) returns the mesh only for Solid and otherwise refuses with the new GeometryError::NotASolid, naming the product. An IfcShellBasedSurfaceModel now compiles (axiolid/kernel#161) but is a Surface: without this a caller summing the divergence of its triangles gets a volume the file never claimed, finite and plausible for a closed shell. A backend that does not report closure gives Unknown, which is refused as well. NetMesh::closure carries the flag for net bodies. compile_product_mesh is unchanged and still returns the bare mesh.
  • tests/meshing_coverage.rs and the generated public fixture test/fixtures/synthetic-coverage/meshing_coverage.ifc (#47): one product for each product-meshing failure kind #47 measured on real models, compiled through compile_product_mesh. The four kinds fixed here (#43 to #46) pin exact volumes, cross-checked with IfcOpenShell 0.8.5. The two fixed in the Axiolid reference compiler (axiolid-mesh-compile 0.3.1) pin their answer too: polygonal faces with more than 3 corners (axiolid/kernel#160) mesh the 2 m quad cube to 8 m3, and the open-shell surface model (axiolid/kernel#161) meshes its 0.12 m2 and refuses any volume.
  • DegenerateFacePolicy (#46), set per session with LoweringSession::with_face_policy. The default, Refuse, is unchanged: an IfcPolyLoop with fewer than three distinct edges refuses the brep, naming the loop. DropAndReport leaves out a face whose outer (or only) bound collapses, since it covers no area, and lists it in ProvenanceMap::dropped_faces. A collapsed hole in a face with real area is still refused, and a shell whose every face collapses is refused as Degenerate. The policy drops exactly what the default refuses.
  • On OfficeBuilding.ifc all 16 IfcWindows refused this way (two shared loops of the form (A, A, B, B)) compile under DropAndReport, each reporting its one dropped face; their volume, 0.1707752 m3, matches IfcOpenShell 0.8.5. No other product in eight real models changes.
  • IfcArbitraryClosedProfileDef and IfcArbitraryProfileDefWithVoids now lower an IfcCompositeCurve outer or inner boundary (#43). Segments may be IfcPolyline, IfcTrimmedCurve over IfcCircle or IfcLine, or a nested IfcCompositeCurve. Arcs stay exact Circle2 segments. SameSense, SenseAgreement and MasterRepresentation are honoured, and a trim may be a parameter (in the project's plane-angle unit) or a cartesian point.
  • On the two real models that carried them, all 128 products refused for a composite profile boundary now compile, and no other product changed.
  • Net geometry (#44, ADR 0014): compile::compile_product_mesh_net (and _with for your own backend) returns a product's Body with every IfcRelVoidsElement opening subtracted, plus the ids of the openings removed. compile_product_mesh is unchanged and stays gross: quantity takeoff wants gross, clearance and ratio checks want net. The graph-level entry point is lower::lower_product_net; the kernel-free relation reader is openings_of.
  • An opening that cannot be removed -- its Body does not lower, it has none, it is not a solid, or the backend refuses it or its cut -- is GeometryError::OpeningNotSubtracted naming the host and the opening. The gross body is never returned in its place. A host whose own Body the backend refuses stays CompilationRefused on the host.
  • Multi-item hosts and openings (a Body with several items, or a mapped item) are cut item by item: every host part minus every opening part. A mapped opening is flattened through its instance transforms, composed outer after inner.

Changed ​

  • The workspace requires axiolid-mesh-compile 0.3.2 and axiolid-mesh-compile-contract 0.3.1. With 0.3.0 an IfcPolygonalFaceSet with any face of more than 3 corners, or with voids (IfcIndexedPolygonalFaceWithVoids), was refused as Unsupported(Tessellation) (axiolid/kernel#160), and every IfcShellBasedSurfaceModel as "brep has no solid" (axiolid/kernel#161). 0.3.1 triangulates such faces in their own plane and refuses a non-planar one by face index. With 0.3.1 a bend trimmed from a circle across its seam, as Revit writes a bar's bends (270 -> 45 or 270 -> 15 degrees), was sampled around the wrong side of the circle, so the bend missed the next leg and the bar was refused as composite directrix has a N unit gap (axiolid/kernel#168). 0.3.2 runs the trim from its first end the way its sense says, wrapping past the seam. On the Revit rebar model below that compiles the last 1,494 refused bars: 40,990 of 41,019 products compile, and no local model refuses a directrix gap.
  • The workspace requires axiolid-construct 0.3.2 (reached through axiolid-mesh-compile, pinned only as a floor behind compile-reference-backend). With 0.3.0 two compiled results were wrong or refused, though lowering was right: an IfcPolygonalBoundedHalfSpace whose Position is translated within the clip plane cut the wrong region with no error (axiolid/kernel#164), and an opening body extruded downward, as Solibri and Revit hang windows from the lintel, was wound inside-out, so its subtraction was refused (axiolid/kernel#166; 78 of 423 real hosts refused with 0.3.0, 14 with 0.3.1). Both tests that pinned these now run. With 0.3.1 a swept disk was oriented by one fixed axis seeded from its first segment: a bent bar whose later leg ran along that axis was refused, and a leg nearly along it twisted the tube so its volume came out low with no error (axiolid/kernel#169). 0.3.2 carries the frame along the path. On the 41,019-product Revit rebar model below, the 878 refused bars compile and swept-disk bars within 0.5 % of their closed-form volume go from 30,199 to 34,003 of 39,215; none is more than 1 % off.
  • A gap between consecutive composite segments, or between the last and the first, wider than 1e-5 m is refused as Degenerate, naming the segment and the gap. It is never bridged with an edge the file did not author.
  • Any other segment parent, a reparametrised segment, and a conic placed with a 3D placement stay typed Unsupported, naming the entity.

Known limits ​

  • The compiled mesh of a curved profile is only as close to the exact area as the kernel's chord budget allows. Up to axiolid-mesh-compile 0.3.2 that budget equals the linear tolerance, which is coarse for small radii: a real gutter profile meshes 1.9 % over its exact area and a slot 0.7 % under (axiolid/kernel#165). Lowering is exact; the arcs reach the kernel as arcs.
  • On the local real-model corpus (423 hosts with openings whose gross Body compiles, six models) 14 hosts are still refused: the kernel boolean refuses a non-manifold operand, and each refusal names the opening. No host that already netted changed volume between kernels.
  • Measured against IfcOpenShell 0.8.5 on 138 sampled hosts across four real models: 130 agree within 0.1 % (median difference about 1e-10). The other 8 are not subtraction errors. On 7, IfcOpenShell closes a gap in the host's composite-curve profile with a segment the file never authored; our gross matches an independent exact integration of the profile to 1e-4 on the 4 checked, IfcOpenShell's is off by up to 7 %. On 1, IfcOpenShell's own boolean fails and it returns the host uncut.

Fixed ​

  • IfcPolygonalBoundedHalfSpace clips now compile (#45). PolygonalBoundary was lowered through the 3D curve path as a Curve3, but Axiolid's BoundedHalfSpace contract and reference compiler require a Curve2, so every such IfcBooleanClippingResult was refused with half-space boundary .. is not a Curve2 node although lowering succeeded. The boundary now lowers as a Curve2 polyline in Position's XY plane, lengths converted to metres. A 3D boundary point is accepted only with z = 0; any other z violates BoundaryDim and is refused as Degenerate, naming the point, instead of being projected.
  • IfcSweptDiskSolid, IfcSweptDiskSolidPolygonal, IfcFixedReferenceSweptAreaSolid and IfcSurfaceCurveSweptAreaSolid read StartParam/EndParam in their directrix's own parameterisation. On an IfcCompositeCurve that is not a length: ISO 10303-42 accumulates each segment's parametric length, 1 per IfcPolyline edge and a trimmed segment's own trim span, so an arc contributes its ANGLE in the file's plane-angle unit (IFC4 IfcCompositeCurve, figure 389: a line plus a 90 degree arc is 91). It was converted as a length and handed to the kernel, which measures arc length, so a Revit rebar authored (0, 365) over five 1-unit legs and four 90 degree bends was read as 365 m and refused, and a range that happened to fit silently cut the bar short. A full range now keeps the authored directrix; a partial range is cut exactly at the composite's parameters before the kernel sees it, honouring SameSense, SenseAgreement, ParamLength and arcs across the circle's seam. A range past the composite's parametric length is Degenerate, naming the sweep and the length; a range over a segment trimmed only by points is Unsupported, since its parametric length is not stated. A trimmed-curve directrix now converts by its basis, like any trim, instead of always as an angle.
  • On the local real-model corpus (eight models) this changes one model: in a 41,019-product Revit rebar model, compiled products go from 19,832 to 38,618. Checked against the closed-form volume (inscribed disk polygon times exact path length) of all 39,215 swept-disk rebars: before, 25 were within 0.5 % and 18,077 compiled wrong, up to 96 % short; with this fix alone (axiolid-construct 0.3.1) 30,199 are within 0.5 %, and 34,003 with the kernel floors above. No product in any other model changed volume.

ifc-model ​

Fixed ​

  • Model::type_histogram no longer lists a type with a count of zero after its last entity is removed (remove), retyped (retype) or replaced under the same id by another type (insert). The emptied type-index bucket is now dropped, so the histogram equals that of a model rebuilt from the same entities (#106). ids_of_type was already correct (an empty slice either way).

Added ​

  • tests/index_coherence_property.rs: seeded random sequences of insert, push, remove, retype, set_attribute(s) and committed, refused and stale transactions, checked after every step against a naive shadow store, a scan, a model rebuilt from scratch and an independent reference walk for ReverseIndex; a refused transaction must leave the model unchanged, revision included (#106). No new dependency: a local SplitMix64 over fixed seeds keeps it deterministic.

ifc-schema ​

Changed ​

  • The express and generation features use the workspace's openbim-step pin, =0.11.0 (was its own =0.10.0), shared with ifc-step, so a build with express links one openbim-step (#288). 0.11 changed no EXPRESS extraction: the bundled tables still match the fetched schemas. No openbim-step type is part of this crate's API, and the default build links none.

ifc-systems ​

Added ​

  • What a system serves (#230). System gains serviced_buildings, read from IfcRelServicesBuildings in IFC2X3, IFC4 and IFC4X3, and serviced_facilities, the IFC4X3 IfcSystem.ServicesFacilities view (the structures whose IfcRelReferencedInSpatialStructure lists the system; empty under IFC2X3 and IFC4, whose RelatedElements cannot hold a system). The admissible target type comes from the declared release's table (IfcSpatialStructureElement in IFC2X3, IfcSpatialElement after). New SystemAnomaly variants report a second relationship for one system (ServicesBuildingsTwice; ServicesBuildings is SET [0:1]) and a target the release does not admit (ServicedNotSpatial); dangling targets are Dangling, a non-system relating end NotASystem.

  • serve_buildings and serve_buildings_with_owner_history stage an IfcRelServicesBuildings in the model's declared release, refusing a relating end that is not an IfcSystem, a target the release's RelatedBuildings does not admit, a missing reference, a repeated target, and a system that already services buildings in the model or on the transaction. A refusal stages nothing.

  • System gains long_name and predefined_type (#231): the IfcDistributionSystem LongName and PredefinedType (IfcDistributionSystemEnum token), read by attribute name in the declared release's table, for IfcDistributionSystem and its subtype IfcDistributionCircuit under IFC4 and IFC4X3. Both are None for every other system type and on IFC2X3, which has no IfcDistributionSystem.

ifc-template-catalog ​

Security ​

  • With the xml feature, require quick-xml 0.42 (was 0.37), which fixes RUSTSEC-2026-0194 (quadratic duplicate-attribute check on one start tag) and RUSTSEC-2026-0195 (unbounded namespace-declaration allocation in NsReader); both are denial of service on untrusted input (#267). PSD and QTO import reads the same values as before: references are resolved, and literal tabs and line breaks in attributes and text are kept.

ifc-validate ​

Added ​

  • The form of every value is checked against ISO 10303-21:2016 (#199): a typed parameter is written exactly where the declared type is a SELECT (§12.1.8), and the bare value everywhere else (§12.1.6, §12.1.7). Three new rule ids, all errors, each pinned by adversarial fixture pairs:

    • type.typed.outside_select: a typed parameter of the declared type, or of a specialisation of it, where the declared type is not a SELECT. IFCAREAMEASURE(12.5) in IfcQuantityArea.AreaValue is reported; it used to pass because the payload was judged against the wrapper alone.
    • type.typed.wrong_type: a typed parameter of another type there. IFCLABEL('x') in AreaValue used to pass as well.
    • type.select.untyped: a bare value that is not a reference where the declared type is a SELECT, such as NominalValue : IfcValue written 1. instead of IFCREAL(1.). Part 21 requires the typed form for every SELECT, not only ambiguous ones. A SELECT of entities alone still reports type.entity.expected_reference.

    All three apply to aggregate members against the element type, and to the parameter inside a typed wrapper against the wrapper's type. Whether the declared type is a SELECT follows defined-type aliases, as §12.1.8 EXAMPLE 2 encodes a type aliasing a SELECT. They are errors, not warnings, although many readers unwrap a well-typed wrapper: the file is not legal, and ifcopenshell.validate rejects both forms. The reasoning is in the type_check module docs.

  • type_check::Mismatch has the variants TypedOutsideSelect, TypedWrongType and UntypedSelectValue.

Changed ​

  • type.select.member walks only nested SELECTs, never the underlying type of a defined type in the select-list: §12.1.8 requires the keyword to name a type the SELECT, or a SELECT nested in it, lists. IFCRATIOMEASURE(0.5) in an IfcColourOrFactor slot, which lists IfcNormalisedRatioMeasure, is now reported.

openbim-ifc ​

Added ​

  • ifc::schema re-exports ifc-schema (feature schema), so the bundled schemas ids_of_type_including_subtypes needs, such as ifc::schema::ifc4(), are reachable without a direct ifc-schema dependency. Found by building a crates.io-only consumer of 0.3.0.

[0.3.0] - 2026-10-14 ​

ifc-alignment ​

Changed ​

  • Breaking: requires Axiolid 0.3. Lowered alignment curves are returned as axiolid_model::GeometryGraph and NodeId, so the major Axiolid version is part of this crate's public API. No code change.

ifc-approval ​

Added ​

  • DateTimeInput: an approval time as IFC4/IFC4X3 IfcDateTime text or an IFC2X3 IfcDateTimeSelect record (From<&str>, From<EntityId>).
  • ApprovalError::NotInSchema, StructuredValue, EntityNotInSchema, AuthoringNotInSchema and AuthoringValueType, and projections' release(). SchemaVersion is re-exported.

Changed (breaking) ​

  • The approval views read every attribute by name in the model's declared release (#212). They read the IFC4 positions from every file, so an IFC2X3 IfcApproval answered its description as the identifier and its date record as the name, and an IFC2X3 IfcApprovalRelationship swapped its ends. ApprovalView binds the header (IFC2X3, IFC4 or IFC4X3; none reads as IFC4) and a lookup refuses IFC4X1, IFC4X2, unknown and multiple schemas. An attribute the release does not declare is NotInSchema (IFC2X3 IfcApproval has no RequestingApproval, GivingApproval, or Status, Level and Qualifier under those names, which are not documented as its ApprovalStatus, ApprovalLevel and ApprovalQualifier), and time_of_approval on IFC2X3's ApprovalDateTime record is StructuredValue with the record id. IFC2X3 RelatedApproval is read as a one-element related_approvals. IFC4 and IFC4X3 answers are unchanged.

  • create_approval, relate_approvals and relate_resource_approval bind the declared release and lay their records out by name (#212). In IFC2X3 an IfcApproval has seven attributes and requires Identifier, Name and ApprovalDateTime (the IFC4 TimeOfApproval) as a date record; an IfcApprovalRelationship takes exactly one related approval and requires Name; IfcResourceApprovalRelationship does not exist (EntityNotInSchema). A value the release does not declare is AuthoringNotInSchema (formerly AuthoringInvalid) and one it cannot hold AuthoringValueType. IFC4 and IFC4X3 records are unchanged.

  • ApprovalDraft::time_of_approval is Option<DateTimeInput>; the setter takes impl Into<DateTimeInput>, so .time_of_approval("…") still compiles.

  • Approval::try_new, ApprovalRelationship::try_new, ResourceApprovalRelationship::try_new and ApprovalAssignment::try_new take the SchemaVersion to read against, refusing IFC4X1, IFC4X2 and an entity the release does not declare. ApprovalView::new is no longer const.

  • ApprovalDraft, ApprovalRelationshipDraft, ResourceApprovalDraft and ApprovalAssociationDraft are #[non_exhaustive] (#214). Struct literals no longer compile outside the crate: build them with ApprovalDraft::new(), ApprovalRelationshipDraft::new(relating_approval, related_approvals), ResourceApprovalDraft::new(related_resources, relating_approval) and ApprovalAssociationDraft::new(global_id, related_objects, relating_approval) plus field-named setters. Fields stay public.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-author ​

Changed (breaking) ​

  • PersonDraft, OrganizationDraft, ApplicationDraft and OwnerHistoryDraft are #[non_exhaustive] (#214), so a later release can add a field without another break. Struct literals no longer compile outside the crate: build each with new(...) and field-named setters, PersonDraft::new(), OrganizationDraft::new(name), ApplicationDraft::new(developer, version, full_name, identifier) and OwnerHistoryDraft::new(owning_user, owning_application, creation_date) (then e.g. .change_action("ADDED")). Fields stay public.

Changed ​

  • Links no bundled schema table itself: every entry point takes the Schema from the caller. A consumer that used a table through this crate's dependency (ifc_schema::ifc4()) enables it on its own ifc-schema dependency (default features bundle every release).
  • A type declaration form ifc-schema adds later resolves as unresolved (no refusal on shape, no form claim) instead of failing to compile; follows ifc_schema::TypeKind becoming #[non_exhaustive].

ifc-classification ​

Changed (breaking) ​

  • ClassificationHierarchy and EffectiveClassifications are #[non_exhaustive].
  • Every public draft is #[non_exhaustive], so struct literals no longer compile outside the crate. Each gains a constructor taking its required fields and one builder setter per other field, named after the field and taking the unwrapped value (.source("NBS") sets Some):
    • ClassificationDraft::new(name)
    • DocumentDraft::new(identification, name)
    • LibraryDraft::new(name)
    • AssociationDraft::new(global_id, related_objects)
    • ExternalReferenceRelationshipDraft::new(relating_reference, related_resources)
    • ClassificationReferenceDraft::new(), DocumentReferenceDraft::new() and LibraryReferenceDraft::new(), which now also derive Default

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • UnsupportedSchema reads "a release this crate has no verified layout for" instead of "no bundled schema table".
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-constraint ​

Added ​

  • DateTimeInput: a creation time as IFC4/IFC4X3 IfcDateTime text or an IFC2X3 IfcDateTimeSelect record (From<&str>, From<EntityId>).
  • ConstraintError::NotInSchema, StructuredValue, EntityNotInSchema, AuthoringNotInSchema and AuthoringValueType, and projections' release(). SchemaVersion is re-exported.

Changed (breaking) ​

  • The constraint views read every attribute by name in the model's declared release (#212). They read IFC4 positions, types and SELECTs from every file. ConstraintView binds the header (IFC2X3, IFC4 or IFC4X3; none reads as IFC4) and a lookup refuses IFC4X1, IFC4X2, unknown and multiple schemas. An attribute the release does not declare is NotInSchema: IFC2X3 IfcMetric.ReferencePath, and IFC2X3 IfcObjective.LogicalAggregator, whose slot holds ResultValues, an IfcMetric, which was reported as a malformed operator. An IFC2X3 CreationTime record is StructuredValue with the record id instead of InvalidValue; IFC2X3's single BenchmarkValues metric is a one-element list; DataValue is checked against the release's own IfcMetricValueSelect and is required in IFC2X3. IFC4 and IFC4X3 answers are unchanged.

  • create_metric, create_objective, relate_resource_constraint and create_reference bind the declared release and lay their records out by name (#212). In IFC2X3 a metric has ten attributes and requires DataValue, ReferencePath is AuthoringNotInSchema, an objective takes exactly one IfcMetric benchmark and no logical aggregator, text CreationTime is AuthoringValueType, and IfcResourceConstraintRelationship and IfcReference are EntityNotInSchema. Enumeration tokens are checked against the release's enumeration (IFC2X3 lacks, for example, INCLUDES and MODELVIEW). IFC4 and IFC4X3 records are unchanged.

  • ConstraintBaseDraft::creation_time is Option<DateTimeInput>; the setter takes impl Into<DateTimeInput>, so .creation_time("…") still compiles.

  • Metric::try_new, Objective::try_new, ResourceConstraintRelationship::try_new and ConstraintAssignment::try_new take the SchemaVersion to read against, refusing IFC4X1, IFC4X2 and an entity the release does not declare. ConstraintView::new is no longer const. A value for an attribute the release does not declare, which associate_constraint could not hit, is AuthoringNotInSchema rather than AuthoringInvalid.

  • Every public draft is #[non_exhaustive], so struct literals no longer compile outside the crate. Each gains a constructor taking its required fields and one builder setter per other field, named after the field and taking the unwrapped value (.description("…") sets Some):

    • ConstraintBaseDraft::new(name, grade)
    • MetricDraft::new(base, benchmark)
    • ObjectiveDraft::new(base, qualifier)
    • ResourceConstraintDraft::new(relating_constraint, related_resources)
    • ConstraintAssociationDraft::new(global_id, related_objects, relating_constraint)
    • ReferenceDraft::new(), which now also derives Default

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-control ​

Changed (breaking) ​

  • ControlDraft and ControlAssignmentDraft are #[non_exhaustive] (#214). Struct literals no longer compile outside the crate: build them with ControlDraft::new() and ControlAssignmentDraft::new(global_id, control, related_objects) plus field-named setters (.name("Permit")). Fields stay public.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-cost ​

Added ​

  • IFC2X3 cost schedules can carry their dates (#214): DateTimeValue carries a date either as IFC4/IFC4X3 IfcDateTime text (DateTimeValue::Text, From<&str>) or as one of the IFC2X3 IfcDateTimeSelect records (Date(CalendarDate), Time(LocalTime), DateAndTime(CalendarDate, LocalTime)). In IFC2X3 create_cost_schedule_with_owner_history stages the IfcCalendarDate, IfcLocalTime and IfcDateAndTime records for SubmittedOn and UpdateDate and references them, only once the schedule itself is accepted. Record forms are checked against the schema's rules before anything is staged (IfcValidCalendarDate with IfcLeapYear, IfcValidTime, and the component ranges), refused with InvalidValue. The types duplicate ifc-schedule's, since sibling domain crates may not depend on each other.
  • Every draft has a constructor and a setter per optional field, named after it: CostItemDraft::new(global_id), CostScheduleDraft::new(global_id), NestingDraft::new(global_id, parent, children), ScheduleAssignmentDraft::new(global_id, schedule, items), QuantityDraft::new(kind, name, value); CostValueDraft keeps monetary and Default and gains setters.

Fixed ​

  • create_cost_schedule_with_owner_history no longer refuses every dated IFC2X3 schedule (#214): a record form is written as the IfcDateTimeSelect IFC2X3 declares. In IFC4 and IFC4X3 a record form is refused with AuthoringValueType, as text is in IFC2X3.

Changed (breaking) ​

  • create_cost_value, create_monetary_unit and create_currency_relationship bind the model's declared release and lay their records out by attribute name (#213). They wrote the IFC4 layout into every model. In IFC2X3 an IfcCostValue has eight attributes and requires CostType, written from category (IFC4 renamed CostType to Category); a composed value (ArithmeticOperator, Components) is refused with AuthoringNotInSchema. IfcMonetaryUnit.Currency is an IfcCurrencyEnum enumerator in IFC2X3 (a label it does not list is AuthoringValueType) and an IfcLabel from IFC4 on. An IFC2X3 IfcCurrencyRelationship has five attributes and requires RateDateTime as an IfcDateAndTime record. IFC4 and IFC4X3 records are unchanged.
  • create_monetary_unit takes the &Model (create_monetary_unit(tx, model, currency)), which it needs to bind the release.
  • CostValueDraft::applicable_date and fixed_until_date are Option<DateTimeValue>, and create_currency_relationship takes rate_date_time: Option<DateTimeValue>: IFC4 and IFC4X3 text as before ("2026-01-01".into(); the draft setters take impl Into<DateTimeValue>), or an IFC2X3 record form, which the writer stages. A form the release does not declare is refused with AuthoringValueType.
  • CostView::schedules returns Result<impl Iterator<Item = CostSchedule>, CostError> and every CostSchedule accessor reads its attribute by name in the model's declared release (#212). The IFC4 positions misread an IFC2X3 IfcCostSchedule: PreparedBy as the predefined type, the SubmittedOn record as the status and SubmittedBy as the identification. identification reads IFC2X3's ID, which IFC4 renamed. A header declaring IFC4X1, IFC4X2 or an unknown release is CostError::UnsupportedSchema, several CostError::MultipleSchemas (new variants); no header reads as IFC4.
  • CostSchedule::submitted_on and update_date return Option<AuthoredDateTime>: IFC4/IFC4X3 text or the IFC2X3 date record (AuthoredDateTime::Record), never None for a stated IFC2X3 date.
  • CostSchedule::new(id, entity, release) takes the release to read against and returns Result, refusing IFC4X1 and IFC4X2.
  • CostValueDraft, CostItemDraft, CostScheduleDraft, NestingDraft, ScheduleAssignmentDraft and QuantityDraft are #[non_exhaustive]: build them with their constructors and setters instead of a struct literal. Their fields stay public to read and assign.
  • CostScheduleDraft::submitted_on and update_date are Option<DateTimeValue<'a>> (were Option<&'a str>); text converts with .into() or through the setters, and IFC4/IFC4X3 output is unchanged.
  • #[non_exhaustive] on the public enums and result structs a later release could extend: CostItemType, CostScheduleType, CostValueKind, QuantityKind, ArithmeticOperator, UnitBasis and Consistency. A match outside the crate needs a wildcard arm, and the structs can no longer be built outside it.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-element-type ​

Added ​

  • TypeDraft fields for the type-specific attributes IFC4 and IFC4X3 require (#214): operation_type and user_defined_operation_type (IfcDoorType), partitioning_type and user_defined_partitioning_type (IfcWindowType), parameter_takes_precedence (both), event_trigger_type and user_defined_event_trigger_type (IfcEventType), and assembly_place (IfcFurnitureType, IFC2X3 too). Tokens are checked against the bound release's enumeration (Invalid otherwise); a value for an attribute the type does not declare is refused with AuthoringNotInSchema; USERDEFINED event_trigger_type without a non-blank user_defined_event_trigger_type is refused (CorrectEventTriggerType). These four types can now be authored in every release that declares them.
  • TypeDraft::new and SupertypeDraft::new, and one builder setter per field, named after it (TypeDraft::new().name("Beam").tag_or_long_description("B-1")).

Fixed ​

  • create_type (and create_supertype), which take no model, no longer write $ into an attribute IFC4X3 requires: a required attribute left unset is refused with AuthoringRequired, staging nothing, as the model-bound writers already did (#214).

Changed (breaking) ​

  • TypeDraft and SupertypeDraft are #[non_exhaustive]: struct literals outside the crate no longer compile. Use new() (or default()) and the setters; the fields stay public for reading and assignment.
  • create_type refuses IfcDoorType, IfcWindowType, IfcEventType and IfcFurnitureType without their required type-specific attribute, where it wrote $ before.
  • Slot6, Family, ElementType and SupertypeKind are #[non_exhaustive]: a match on Family or Slot6 needs a wildcard arm, and catalogue rows can no longer be built by struct literal outside the crate (use the generated constants).

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-geometry ​

Added ​

  • Lowering an IfcTriangulatedFaceSet now attaches texture coordinates from its IfcIndexedTriangleTextureMap as a corner-indexed attribute channel named uv (lower::tessellated::UV_CHANNEL), one (s, t) per triangle corner (#30). Positions stay shared, so the mesh stays closed. A shorter TexCoordIndex leaves trailing triangles UNMAPPED; an omitted one adds no channel; a longer one, or an index outside the texture vertices, is an error. A second map on the same face set becomes uv1, and so on.

Known limits ​

  • IfcIndexedPolygonalTextureMap (IFC4X3) is not lowered.

Changed ​

  • Breaking: requires Axiolid 0.3 (axiolid-mesh 0.3 adds AttributeChannel::corner_indices).

ifc-georef ​

Changed ​

  • Breaking: requires Axiolid 0.3. ProjectToMap::transform is an axiolid_core::Transform3, so the major Axiolid version is part of this crate's public API. No code change.

ifc-material ​

Changed (breaking) ​

  • Views and authoring bind to the release the model's header declares (#77). Every slot position comes from that release's bundled ifc-schema table (IFC2X3 TC1, IFC4 ADD2 TC1 or IFC4X3 ADD2) instead of IFC4 constants, so an IFC2X3 model is no longer read with IFC4 slots. Behaviour for an IFC4 model, and for an in-memory model whose header declares no schema (bound to IFC4 as before), is unchanged, except where listed below. Breaking, because the same call now answers differently for IFC2X3 and IFC4X3 models:
    • an accessor for an attribute the release does not declare returns MaterialError::NotInSchema instead of Ok(None). For IFC2X3: Material::{description, category}, MaterialLayer::{name, description, category, priority}, MaterialLayerSet::description and MaterialLayerSetUsage::reference_extent. A typed absence rather than None, because None claims the file left the value unset;
    • a record the release cannot instantiate -- an IfcMaterialConstituent(Set), IfcMaterialProfile*, IfcMaterialLayerWithOffsets or IfcMaterialRelationship in an IFC2X3 model, or IFC2X3's abstract IfcMaterialProperties -- is refused with MaterialError::EntityNotInSchema by its accessors, resolve_material_select and assigned_material;
    • an IFC2X3 IfcMaterialLayer.LayerThickness of zero is invalid (IfcPositiveLengthMeasure; IFC4 relaxed it to IfcNonNegativeLengthMeasure);
    • the IfcRelDefinesByType fallback admits the concrete IfcTypeObject subtypes of the model's release, so IFC4X3 no longer accepts the IfcDoorStyle/IfcWindowStyle it removed. The generated IFC4 list is gone;
    • a header declaring several schemas fails every read and write with MaterialError::MultipleSchemas, and one declaring an unknown schema with MaterialError::UnsupportedSchema.
  • create_material takes the &Model it writes into and returns MaterialResult<EntityId>, like every other authoring function: without the model it cannot know the release, and IFC2X3 IfcMaterial has no Description or Category slot to write.
  • Authoring writes the release's layout: IFC2X3 gets the short IfcMaterial, IfcMaterialLayer, IfcMaterialLayerSet and IfcMaterialLayerSetUsage records. A draft value the release cannot hold (an IFC2X3 layer name, category or priority, a layer set description, a usage reference extent, a material description or category) is refused with MaterialError::AuthoringNotInSchema rather than dropped; an entity it lacks is refused with MaterialError::EntityNotInSchema; and a required attribute left unset -- the IFC2X3 IfcRelAssociatesMaterial.OwnerHistory -- with MaterialError::AuthoringRequired, so associate_material refuses an IFC2X3 model. Nothing is staged on refusal.
  • IFC4X3 is read against its own table. Its material entities keep the IFC4 layouts; the one rename, IfcMaterialRelationship.Expression to MaterialExpression at the same position, is resolved by name.
  • ifc-schema is now a production dependency.

Fixed ​

  • associate_material, create_material_properties and layer-set members accept exactly what the release's schema accepts: RelatingMaterial any instantiable IfcMaterialSelect member (an IfcMaterialLayer too; IFC2X3 has no constituent or profile sets), Material any IfcMaterialDefinition, and no IfcMaterialLayerWithOffsets in an IFC2X3 set.

Added ​

  • material_schema(model), MaterialView::schema() and a schema() accessor on every projection report the bound release; SchemaVersion is re-exported.

  • try_from_view(view, id) on every projection looks a record up and binds it to the model's release. try_new has no header and keeps reading IFC4.

  • associate_material_with_owner_history takes a caller-supplied IfcOwnerHistory, which IFC2X3 requires; none is ever invented.

  • MaterialError::{MultipleSchemas, UnsupportedSchema, NotInSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringRequired}.

  • MaterialView::constituent_fraction_diagnostic: an opt-in policy check that an IfcMaterialConstituentSet's fractions describe one whole (#103). It returns a ConstituentFractionDiagnostic when every constituent states a fraction but the sum is further from 1 than the caller's tolerance (SumNotOne), or when stated and missing fractions are mixed (PartiallyStated). IFC4 declares no WHERE rule on the sum, so this is never a decode error and never normalises: the accessors keep returning the authored fractions.

ifc-model ​

Changed ​

  • tests/package_architecture.rs enumerates the allowed bridge-to-bridge dependencies (BRIDGE_EDGES: only ifc-geometry -> ifc-alignment) instead of allowing any bridge pair, and a new test requires the manifests to match that list exactly (#143, ADR 0003 amendment 2026-09-28). No crate API changes.

Changed (breaking) ​

  • Conflict and Stop are #[non_exhaustive]: a match needs a wildcard arm, so a new commit-conflict or walk-stop reason is not a breaking change.
  • Applied is #[non_exhaustive]: read its fields; it can no longer be built with a struct literal outside the crate.

ifc-occurrence ​

Added ​

  • OccurrenceDraft fields for what IFC2X3 TC1 requires of a few classes (#214): shape_type (IfcRamp, IfcRoof, IfcStair), nominal_diameter and cross_section_area (IfcReinforcingBar, IfcTendon), bar_role (IfcReinforcingBar), and longitudinal_bars and transverse_bars (IfcReinforcingMesh), each a new MeshBars of nominal diameter, cross-section area and spacing. These IFC2X3 records can now be authored with create_with_owner_history; IFC4 and IFC4X3, which declare the measures OPTIONAL, write them when given. A value for an attribute the bound release does not declare on the class is refused with AuthoringNotInSchema.
  • OccurrenceError::UnknownToken (a ShapeType or BarRole outside the release's enumeration), InvalidMeasure (a non-positive or non-finite IfcPositiveLengthMeasure, a non-finite IfcAreaMeasure) and TypeClassNotInSchema (the bound release pairs no type class with the occurrence, such as an IFC2X3 IfcStair), appended.
  • OccurrenceError implements Display and std::error::Error.
  • OccurrenceDraft::new and one builder setter per field, named after it.
  • Occurrence::ifc4_type_class and Occurrence::ifc2x3_type_class: the type class IFC4 ADD2 TC1 and IFC2X3 TC1 pair with each class, generated from their EXPRESS sources by scripts/gen-occurrences.py.

Fixed ​

  • The occurrence-to-type pairing follows the declared release (#214). It was IFC4X3's CorrectTypeAssigned in every release, so an IFC2X3 IfcDoor typed by an IfcDoorStyle was refused; IFC2X3 now pairs doors and windows with IfcDoorStyle and IfcWindowStyle and every other class with the later releases' type class where IFC2X3 declares it, and IFC4 uses its own rules (IFC4's IfcTransformer rule names the undeclared IFCTRANFORMERTYPE, an erratum recorded as written). The referenced type is compared with TYPEOF semantics, subtypes included.

Changed (breaking) ​

  • OccurrenceDraft is #[non_exhaustive]: struct literals outside the crate no longer compile. Use new() (or default()) and the setters; the fields stay public.
  • Occurrence, the generated catalogue row, is #[non_exhaustive] and has two new fields; it can no longer be built by struct literal outside the crate (use the generated constants).
  • An IFC2X3 or IFC4 typed_by is checked against that release's pairing: a class IFC2X3 pairs with nothing (its type class undeclared there) is refused with TypeClassNotInSchema where it was checked against the IFC4X3 class, and WrongTypeClass.expected names the release's class.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-properties ​

Added ​

  • exact_unit(model, measure_type, explicit_unit) resolves a measure's effective unit to an exact SI conversion (#53). It uses the explicit unit, otherwise the project default, and returns value_si = value * scale + offset with the SI dimensional exponents. Conversion-based chains, derived units, the gram, and degrees Celsius are resolved. Count and ratio measures get a dimensionless answer.
  • It refuses rather than guesses (ExactUnitError):
    • unknown prefixes, unit names or measure types
    • duplicate or missing project units
    • cyclic or over-deep conversion chains
    • a unit whose name, declared Dimensions or factor unit contradicts its type
    • an explicit unit of the wrong type
    • offset units
  • It binds to the declared release like exact_property and reads that release's IfcDimensionsForSiUnit/IfcCorrectDimensions. They differ: IFC2X3 TC1's farad fails its own capacitance check, and it is reported as such.
  • UnitKind::Conversion::offset keeps IfcConversionBasedUnitWithOffset.ConversionOffset, which was dropped.

Changed (breaking) ​

  • UnitKind::Si::prefix_exponent is now Option<i32>. An unrecognised prefix is None instead of 0, which read .BOGUS. as "unprefixed" and scaled by 1.0.
  • UnitKind::si_scale raises the prefix to the unit's power: MILLI SQUARE_METRE is 1e-6 (was 1e-3), and MILLI CUBIC_METRE is 1e-9. It is None for an unknown prefix.
  • UnitKind::Conversion gains the offset field, so exhaustive patterns must name it or use ...

ifc-resource ​

Changed (breaking) ​

  • ActorRoleDraft, PostalAddressDraft, TelecomAddressDraft, ActorDraft, AssetDraft, InventoryDraft, AppliedValueDraft, ResourceDraft, ResourceTimeDraft, AllocationDraft and the resource NestingDraft are #[non_exhaustive] (#214). Struct literals no longer compile outside the crate: build each with new(...) and field-named setters. New constructors: ActorRoleDraft::new(role), PostalAddressDraft::new(), TelecomAddressDraft::new(), ActorDraft::new(global_id, the_actor), AssetDraft::new(global_id), InventoryDraft::new(global_id) and AppliedValueDraft::new(), each with a setter per remaining field. The drafts that already had builders keep them unchanged. Fields stay public where they were.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.
  • Enumeration checks treat a type declaration form ifc-schema adds later as not matching; follows ifc_schema::TypeKind becoming #[non_exhaustive].

ifc-schedule ​

Added ​

  • create_lag_time_in(tx, model, name, lag_value, duration_type): the model-bound create_lag_time. IFC2X3 declares no IfcLagTime and is refused with EntityNotInSchema, nothing staged; IFC4 and IFC4X3 stage exactly what create_lag_time stages. create_lag_time documents that it is for IFC4 and IFC4X3 only (#211).
  • IFC2X3 work plans and work schedules can be authored (#214): DateTimeValue carries a date either as IFC4/IFC4X3 IfcDateTime text (DateTimeValue::Text, From<&str>) or as one of the IFC2X3 IfcDateTimeSelect records (Date(CalendarDate), Time(LocalTime), DateAndTime(CalendarDate, LocalTime)). In IFC2X3 create_work_control_with_owner_history stages the IfcCalendarDate, IfcLocalTime and IfcDateAndTime records and references them, only once the work control itself is accepted. Record forms are checked against the schema's rules before anything is staged (IfcValidCalendarDate with IfcLeapYear, IfcValidTime, and the ranges of IfcMonthInYearNumber, IfcHourInDay, IfcMinuteInHour and IfcSecondInMinute), refused with InvalidValue. CalendarDate and LocalTime are #[non_exhaustive]; LocalTime does not yet carry Zone or DaylightSavingOffset, which are written $.
  • IFC2X3 sequences can be authored (#214): TimeLag::Seconds is the IFC2X3 IfcRelSequence.TimeLag : IfcTimeMeasure, TimeLag::LagTime the IFC4/IFC4X3 IfcLagTime reference. A non-finite lag is refused with InvalidValue.
  • ProcedureDraft::user_defined_procedure_type, written as the IFC2X3 IfcProcedure.UserDefinedProcedureType (#214), so a USERDEFINED IFC2X3 procedure can be authored.
  • Every draft has a new constructor taking its required fields (TaskDraft::new(global_id), WorkControlDraft::new(global_id, creation_date, start_time), EventDraft::new(global_id), ProcedureDraft::new(global_id), RecurrenceDraft::new(recurrence_type), EventTimeDraft::new(), TaskTimeDraft::new()) and a setter per other field, named after it, as ifc-resource's drafts are built.

Fixed ​

  • create_work_control_with_owner_history, create_sequence_with_owner_history and create_procedure_with_owner_history no longer refuse the IFC2X3 records the drafts could not carry (#214). IfcProcedure.WR4 is enforced instead: an IFC2X3 USERDEFINED procedure without a non-blank user_defined_procedure_type is refused with InvalidValue.
  • The release-bound writers refuse a SchemaVersion this build carries no table for with UnsupportedSchema instead of panicking.

Changed (breaking) ​

  • The task, work-control and sequence readers bind the model's declared release and read every attribute by name from its table (#212). They read IFC4 slot constants from every file, so an IFC2X3 task answered its Status as the long description, WorkMethod as the status and Priority as the milestone flag, and an IFC2X3 work plan or schedule its WorkControlType as the predefined type. The header binds IFC2X3, IFC4 or IFC4X3 (none reads as IFC4); IFC4X1, IFC4X2, unknown and multiple schemas are refused with the new ScheduleReadError. IFC4 and IFC4X3 answers are unchanged.
  • tasks, work_plans, work_schedules, sequences, predecessors_of, successors_of, start_tasks, end_tasks, tasks_of_schedule and subtasks_of return Result<_, ScheduleReadError>; find_cycle returns Result<Option<SequenceCycle>, ScheduleReadError>; downstream_of and execution_order return ScheduleReadError::Cycle(SequenceCycle) for a loop instead of a bare SequenceCycle.
  • Task::new(id, entity, release) and WorkControl::new(id, entity, release) take the SchemaVersion to read against and return Result, refusing IFC4X1 and IFC4X2; WorkControl::new is Ok(None) for another entity. Task::release and WorkControl::release report the binding.
  • An attribute IFC2X3 does not declare reads as None: Task:: long_description, predefined_type and task_time_ref, and WorkControl::predefined_type. Task::identification reads IFC2X3's TaskId and WorkControl::identification its Identifier, which IFC4 promoted to Identification. WorkControl::work_control_type reads IFC2X3's WorkControlType, which is not aliased to PredefinedType.
  • WorkControl::creation_date, start_time and finish_time return Option<AuthoredDateTime> (IFC4/IFC4X3 text, or the IFC2X3 IfcDateTimeSelect record), and duration and total_float return Option<AuthoredDuration> (IFC4/IFC4X3 text, or the IFC2X3 IfcTimeMeasure), instead of None for a stated IFC2X3 value.
  • Sequence gains time_lag_measure, IFC2X3's IfcRelSequence.TimeLag (an IfcTimeMeasure on the relationship); lag stays the IFC4/IFC4X3 IfcLagTime.
  • TaskDraft, WorkControlDraft, EventDraft, EventTimeDraft, RecurrenceDraft, ProcedureDraft and TaskTimeDraft are #[non_exhaustive]: build them with new and the setters instead of a struct literal. Their fields stay public to read and assign.
  • WorkControlDraft::creation_date and start_time are DateTimeValue<'a> (were &'a str) and finish_time is Option<DateTimeValue<'a>> (was Option<&'a str>). The plain create_work_control refuses a record form with InvalidValue; with text its output is unchanged.
  • create_sequence_with_owner_history takes time_lag: Option<TimeLag> (was Option<EntityId>); wrap an IfcLagTime id in TimeLag::LagTime. The plain create_sequence is unchanged.
  • ProcedureDraft has the new user_defined_procedure_type field; the plain create_procedure, which writes IFC4/IFC4X3, refuses a value for it with InvalidValue, and the release-bound writer refuses it in IFC4 and IFC4X3 with AuthoringNotInSchema.
  • #[non_exhaustive] on the public enums and result structs a later release could extend: SequenceType, DurationType, TaskTimeAnomaly, WorkControlKind, WorkTimeRole, RecurrenceType, Lag, Sequence, SequenceCycle, EventTime, Recurrence and WorkTime. A match outside the crate needs a wildcard arm, and the structs can no longer be built outside it.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-schema ​

Changed (breaking) ​

  • Artifact format 3 records the facts above; format 1 and 2 artifacts still decode, with those facts empty.
  • A nested aggregate attribute's type_name is its innermost element type (IfcLengthMeasure for LIST OF LIST OF IfcLengthMeasure), where the old extractor recorded the inner keyword LIST.
  • The express and generation features use openbim-step =0.10.0 (the runtime links none).
  • One cargo feature per bundled release (#112): ifc2x3, ifc4, ifc4x1, ifc4x2 and ifc4x3, all in default, so a default build bundles every release as before. Each accessor (ifc2x3(), ifc4(), ...) exists only with its feature. ifc4 used to ship all bundled tables; it now ships IFC4 only, so a build with default-features = false, features = ["ifc4"] loses the other releases -- name them, or keep defaults.
  • for_version returns Result<&Schema, NotBundled> instead of Option<&Schema>, and exists in every build. Err(NotBundled) means a recognised release whose feature is off; an unknown FILE_SCHEMA token is still None from SchemaVersion::from_header_token, so the two cases stay distinguishable.
  • write_structural_catalog and write_direct_structural_catalog exist in every build and return an io::ErrorKind::Unsupported error wrapping NotBundled for a release that is not compiled in, instead of panicking.
  • artifact_decode_schema and BundledSchemaError need the new artifact feature (enabled by every release feature) instead of ifc4.
  • SchemaVersion is #[non_exhaustive], derives Hash, and gains Ifc4x1 and Ifc4x2 (#33). A match on it needs a wildcard arm; a consumer should refuse a release it has not verified, never alias it to a neighbour. write_structural_catalog and write_direct_structural_catalog accept the new versions.
  • ifc-schema owns its schema types. Attribute, EntityDef, TypeDef, TypeKind and the newly exported WhereRule are defined here instead of re-exported from openbim_step::express, and all five are #[non_exhaustive]: construct them with Attribute::new, EntityDef::new, TypeDef::new, WhereRule::new and the builder methods (with_supertype, with_attribute, with_derived, with_where_rule, abstract_entity, optional, aggregate), and give every match on TypeKind a wildcard arm. Field names, supertype(), is_derived() and is_defined() are unchanged. Rationale: an openbim-step release no longer ripples into this crate's public API, and later facts about a declaration (aggregate bounds, INVERSE, UNIQUE) can be added as fields without another break.
  • openbim-step is an optional dependency, linked only by the new express feature and by generation. The bundled tables decode straight into the owned types; the default build no longer links a parser.
  • Schema::from_express and Schema::from_express_bytes require the new express feature.
  • Removed: Schema::from_parsed(ParsedSchema) (use Schema::new(name, entities, types)), Schema::graph() (the openbim_step::SchemaGraph it returned is no longer held; Schema answers the same queries itself), and the express module with its parse/ParsedSchema re-exports (use openbim_step::express directly).
  • EntityDef no longer carries redeclared/is_redeclared(). No bundled table ever recorded explicit redeclarations (the artifact format drops them), so they were always empty for ifc2x3(), ifc4() and ifc4x3().
  • artifact_decode_schema returns a Schema and artifact_encode_schema (generation) takes one, instead of openbim_step::express::ParsedSchema.

Added ​

  • Aggregate bounds, nested aggregation, INVERSE and UNIQUE (#111): Attribute::aggregation (levels outermost first, each an Aggregation with AggregateKind, lower and upper Bound, unique, optional_elements), EntityDef::inverses (InverseAttribute) and EntityDef::unique_rules (UniqueRule), with builders. Additive: the types were already #[non_exhaustive]. All five bundled tables are regenerated with them: 115/153/158/160/165 INVERSE and 17/4/4/4/4 UNIQUE declarations for IFC2X3/IFC4/IFC4X1/IFC4X2/IFC4X3, pinned by tests.
  • NotBundled, SchemaVersion::is_bundled() and SchemaVersion::feature_name().
  • IFC4X1 FINAL and IFC4X2 FINAL (#33): bundled tables data/ifc4x1-final.bin (801 entities, 400 types) and data/ifc4x2-final.bin (816 entities, 407 types), generated from the official EXPRESS files like the other three; accessors ifc4x1() and ifc4x2(); for_version returns them; header tokens IFC4X1 and IFC4X2 (the files' own SCHEMA names); release ids IFC4X1_FINAL and IFC4X2_FINAL. Tests pin that both counts differ from IFC4 and IFC4X3 and that each carries its own release's entities.
  • SchemaVersion::ALL, every known version oldest first.
  • Schema::new, Schema::entities() and Schema::types() (declarations in source order), PartialEq/Eq for Schema, and the BundledSchemaError export.

Unchanged ​

  • The bundled artifacts are byte-identical: regenerating all three with the ported generator reproduces the committed files, and FORMAT_VERSION stays 2.

Changed ​

  • Requires openbim-step 0.7.0, matching ifc-step. Both pin the parser exactly, so the pair must move together. openbim-step 0.6 replaced EntityDef::supertype (a field) with supertypes plus a supertype() accessor for multiple inheritance; IFC schemas are single-inheritance, so the serialized artifact is unchanged.

ifc-spatial ​

Added ​

  • SpatialDraft::interior_or_exterior: the IFC2X3 IfcSpace. InteriorOrExteriorSpace (IfcInternalOrExternalEnum), which that release requires, so create_spatial_element_with_owner_history can author an IFC2X3 space (#214). IFC4 and IFC4X3 do not declare the attribute: a value there, or on a container other than a space, is refused with AuthoringNotInSchema, a token outside IFC2X3's enumeration with AuthoringValueType, and the plain IFC4/IFC4X3 create_spatial_element refuses it with AuthoringNotInSchema instead of dropping it.
  • create_project_with_owner_history takes the project's representation contexts, so an IFC2X3 IfcProject, which requires RepresentationContexts (and UnitsInContext), can be authored (#214). Each context must resolve in the model or on the transaction (MissingReference), be an IfcRepresentationContext (WrongReferenceType), not be an IfcGeometricRepresentationSubContext (IFC2X3 WR32, IFC4 and IFC4X3 CorrectContext), and not repeat (Invalid); an empty slice leaves the attribute $, which IFC2X3 refuses with AuthoringRequired.
  • A constructor and builder setters on every draft: SpatialDraft::new(), FacilityDraft::new(), ExternalSpatialDraft::new() and ProjectLibraryDraft::new() start empty; BoundaryDraft::new(space, element, physical_or_virtual, internal_or_external) takes the four attributes the schema requires. Each optional field has a setter of the same name taking the unwrapped value (SpatialDraft::new().name("L1") .composition("ELEMENT")), following ifc-resource's drafts.

Fixed ​

  • A model whose declared release has no bundled table is refused with UnsupportedSchema by the release-bound writers instead of panicking.

Changed (breaking) ​

  • assign_to_actor, assign_to_process, connect_with_realizing_elements and interfere_elements take the &Model and bind its declared release (#213). They wrote records with the wrong number of attributes: seven of IfcRelAssignsToActor's and IfcRelAssignsToProcess's eight, eight of IfcRelConnectsWithRealizingElements's nine, and ten for IfcRelInterferesElements where IFC4 declares nine. Each record is now laid out by attribute name with the release's own arity (the interference has ten attributes in IFC4X3). IFC4 records gain a trailing $ for the first three (ActingRole, QuantityInProcess, ConnectionType) and the IFC4 interference loses its trailing $; IFC4X3 records of the first three gain the same trailing $, and its interference is unchanged. An IFC2X3 model, which requires OwnerHistory (and declares no IfcRelInterferesElements), and a header binding no single verified release are refused; use the *_with_owner_history variants in IFC2X3.
  • SpatialDraft, FacilityDraft, BoundaryDraft, ExternalSpatialDraft and ProjectLibraryDraft are #[non_exhaustive]: struct literals and ..Default::default() updates no longer compile outside the crate; build them with new and the setters. Fields stay public for reading and assignment.
  • SpatialDraft has a new field, interior_or_exterior.
  • create_project_with_owner_history(tx, model, global_id, name, units, representation_contexts, owner_history): the new representation_contexts: &[EntityId] parameter sits before owner_history. Pass &[] for the previous IFC4/IFC4X3 record.
  • #[non_exhaustive] on the public read-side and catalogue types a later IFC release could extend: SpaceBoundary, BoundaryPhysicality, BoundaryExposure, Relationship, RelationshipKind, SpatialKind, SpatialNode, BoundaryLevel and Facility. Matches on the enums need a wildcard arm outside the crate, and the structs can no longer be built by literal there.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • The spatial classifier binds no release for an IFC4X1 or IFC4X2 header (release() is None) and answers from the verified tables.
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-step ​

Changed (lazy loading) ​

  • A strict read loads lazily (ADR 0015). Every record is framed with openbim_step::scan, parsed from its own span and checked against every rule the IFC conversion enforces -- in parallel on inputs of 4 MB and more -- but no value is built; each entity is decoded on first access by the same conversion the eager read uses. The model keeps the source bytes. On seven real IFC files (18-109 MB) opening is 2.1-4.6x faster at 35-76% less peak memory; decoding everything afterwards on 8 threads is still 1.3-2.5x faster than the eager read at the same memory. Results are unchanged: lazy and eager models (and errors) are identical on the 2,273 files of the parse-limits corpus, and any record that fails validation sends the whole file through the eager reader, so an error is always the eager reader's error. StepReader::eager keeps the old behaviour; recovery and reference-check options read eagerly.
  • read_path reads the file into a buffer the model owns instead of memory-mapping it: a lazy model decodes from its source for its whole lifetime, and a mapping of a file changed meanwhile is undefined behaviour. The mapped read is the new unsafeStepReader::read_path_mapped, whose contract says so.
  • Index is rebuilt on openbim_step::scan (breaking). Framing is strict -- junk between records, a missing ENDSEC or a second appended file are errors instead of being skipped -- and ids are found by binary search instead of a linear scan. Index::scan, materialize and materialize_closure return Result, and Index::entity returns Result<Option<Entity>>, where they used to swallow decode failures (a failed materialize returned an empty model). Subsets carry the file's header instead of a synthetic IFC4 one, and Index::header exposes it.
  • Requires openbim-step 0.8.0.

Added ​

  • StepReader::eager, StepReader::read_path_mapped (unsafe) and Codec::read_owned for both codec types.

Changed ​

  • Requires openbim-step 0.7.0, matching ifc-schema. Both pin the parser exactly, so the pair must move together.
  • Reading STEP builds the model from openbim-step's borrowed events, so each value is allocated once, directly in its model form, instead of once as a parser String and again as the model's Arc<str>; records are consumed instead of cloned. The model is identical (checked over 2,273 files); reading takes 22-40% fewer instructions and 13-35% fewer cycles on seven real IFC files, with resident memory unchanged.

ifc-structural ​

Changed (breaking) ​

  • MemberConnection and ActivityAssignment are #[non_exhaustive].
  • Every public draft is #[non_exhaustive], so struct literals no longer compile outside the crate. Each gains a constructor taking its required fields and one builder setter per other field, named after the field and taking the unwrapped value (.name("Frame") sets Some; String fields take impl Into<String>):
    • AnalysisModelDraft::new(global_id, predefined_type)
    • StructuralRootDraft::new(global_id)
    • RelationshipRootDraft::new(global_id)
    • MemberDraft::new(root, kind), ConnectionDraft::new(root, kind)
    • ActionDraft::new(root, applied_load, coordinate_system, kind)
    • ReactionDraft::new(root, applied_load, coordinate_system, kind)
    • LoadGroupDraft::new(global_id, action_type, action_source, kind)
    • ResultGroupDraft::new(global_id, theory_type, is_linear)
    • MemberConnectionDraft::new(root, member, connection)
    • ActivityAssignmentDraft::new(root, relating_element, activity)
    • BoundaryConditionDraft::new()

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.
  • Enumeration checks treat a type declaration form ifc-schema adds later as not matching; follows ifc_schema::TypeKind becoming #[non_exhaustive].

ifc-style ​

Added ​

  • IndexedTextureMap::triangle_coordinates(triangle_count) resolves an IfcIndexedTriangleTextureMap to (s, t) coordinates for each triangle corner, in CoordIndex order. Corners, not vertices: one position may carry a different coordinate in each triangle that uses it. A shorter TexCoordIndex covers only the leading triangles, a longer one is an error, and an omitted one returns None because the schema does not define it (#30).

Fixed ​

  • Breaking: IndexedTextureMap::maps now returns Vec<EntityId>. Maps is LIST [1:?] OF IfcSurfaceTexture, but it was read as a single reference, so it returned an error on every conforming file, including those this crate writes itself.

ifc-systems ​

Changed (breaking) ​

  • The authoring draft ClassifiedSystemDraft is #[non_exhaustive]: build it with ClassifiedSystemDraft::new() and the setters description, object_type, predefined_type and long_name. Fields stay public.

  • The bulk readers no longer fall back to the IFC4 table. systems, ports, zones, spatial_placements, role_inconsistencies, ConnectionGraph::build and ElementRole::of return Result<_, SchemaResolutionError> and refuse a header that binds no verified release: no FILE_SCHEMA (an in-memory model now needs a header), several, IFC4X1/IFC4X2, or a release this build does not bundle. ElementRole::of returns Result<Option<_>, _>.

  • try_zones is removed: zones now has the error channel it added.

  • IFC4X3 is verified for every reader (#215), not only the zone readers: schema_of resolves it. Every fixed slot the readers use is pinned against the IFC2X3, IFC4 and IFC4X3 tables, and tests/ifc4x3.rs round-trips systems (including IfcBuiltSystem), both port attachments, connections, flow roles and spatial placements through IFC4X3 STEP text.

  • SystemAnomaly and RoleInconsistency are #[non_exhaustive]: a match needs a wildcard arm.

  • The read-side Connection, Port, System, Zone and SpatialPlacement are #[non_exhaustive], so a later release's attribute can be added without a breaking change; they can no longer be built with a struct literal outside the crate.

Changed ​

  • Depends on ifc-schema with its default features named explicitly (every bundled release), now that the workspace dependency turns them off for the facade's per-release features (#112).
  • A model whose header declares IFC4X1 or IFC4X2 is refused with the existing unsupported-schema error. ifc-schema now bundles both releases, but no layout here is verified against them, so they are never read as IFC4 or IFC4X3.

ifc-tabular ​

Changed (breaking) ​

  • SeriesDraft and ColumnDraft are #[non_exhaustive] (#214). Struct literals no longer compile outside the crate: build them with SeriesDraft::new(name, start_time, end_time, data_type, data_origin) and ColumnDraft::new() plus field-named setters. Fields stay public.

Changed ​

  • Links no bundled schema table itself: every entry point takes the Schema from the caller. A consumer that used a table through this crate's dependency (ifc_schema::ifc4()) enables it on its own ifc-schema dependency (default features bundle every release).

ifc-template-catalog ​

Added ​

  • corrected_catalog(CatalogEdition::Ifc4x3Add2): an IFC4X3 ADD2 corrected profile whose one patch, NEH-IFC4X3-PSD-0001, adds Pset_Stationing.HasIncreasingStation (IfcBoolean). The published ADD2 documentation (6.6.4.10) lists it; the PSD XML the official snapshot is generated from omits it, and the official snapshot is unchanged (#216).
  • PatchOperation::AddProperty and PatchError::NotAPropertySet.

Changed (breaking) ​

  • ValidationIssue, ValidationReport, CatalogDiagnostic, ExportSummary, Advisory, AppliedPatch and UnresolvedApplicability are #[non_exhaustive]; they can no longer be built with a struct literal outside the crate.

ifc-validate ​

This release is breaking (0.2 -> 0.3): Severity gains a variant, Summary gains a field, RuleEntry gains a field, Mismatch gains a variant, three rule ids are renamed, and Budget::max_depth is removed.

Added ​

  • Severity::EvaluationError and Finding::evaluation_error: an implemented rule that applies to an instance but cannot be decided for it -- the schema tables lack an attribute the rule reads, an operand has a shape the rule cannot reason about, or a target the rule must type-test is absent -- is now reported under the rule's own id instead of being skipped silently (#115). Summary::evaluation_errors counts them, and Report::is_conformant is false while any is present. An unset ($) operand is still not an evaluation error: optional operands are guarded by the rules themselves and mandatory ones are structure.required.missing.
  • Every rule id the crate emits is pinned by an adversarial pair of fixtures, and an inventory read from the crate's source fails the build when a new id ships without one (#114).
  • type.entity.expected_reference: a value that is not an entity reference in a slot only a reference can fill -- an entity-typed slot, a member of an aggregate of entities, or a SELECT whose alternatives are all entities -- is reported (#113). A string in IfcRelSequence.RelatingProcess used to get no structure or type finding at all.

Changed ​

  • Breaking: Severity is #[non_exhaustive] and has the new EvaluationError variant, ordered after Error; exhaustive matches outside the crate must add a wildcard arm.
  • Breaking: Summary is #[non_exhaustive] and has the new evaluation_errors field; its Display now also prints that count.
  • Breaking: RuleEntry has a new releases field, and RuleEntry::applies_to says whether a schema's release declares the rule. The engine now takes every rule's scope from its registry entry: it runs only under the releases listed there, on the entry's entity and its subtypes (#139).
  • Breaking: rule ids now always name the entity that declares the rule, with the label the release uses (#139): IfcRelAssignsToGroupByFactor.NoSelfReference is IfcRelAssignsToGroup.NoSelfReference; IfcPhysicalSimpleQuantity.WR21 is IfcQuantityLength.WR21; and IfcPolyLoop.WR21 is reported only under IFC2X3, with IFC4 and IFC4X3 reporting the same unsupported predicate as IfcPolyLoop.AllPointsSameDim.
  • Breaking: type_check::Mismatch is #[non_exhaustive] and has the new ExpectedReference variant (#113). type_check::check_value now checks aggregate members against the element type, and reports a reference written where the declared type resolves to a primitive.
  • IfcExternalReference.WR1 reads ItemReference under IFC2X3 and Identification under IFC4/IFC4X3, as each release's EXPRESS declares, instead of whichever of the two resolved.

Removed ​

  • Breaking: Budget::max_depth. Nothing read it: every walk the crate performs is over the bundled schema's type graph, which a file cannot lengthen, and the SELECT walk's own bound is now proven sufficient for every SELECT the bundled schemas use. Budget::max_findings remains the one file-controlled limit.

Fixed ​

  • Native WHERE rules run on the entities and releases the schema declares them for (#139). NoSelfReference now checks plain IfcRelAssignsToGroup, which was never checked; IfcMaterialLayer.NormalizedPriority now checks IfcMaterialLayerWithOffsets; and IfcRelDefinesByProperties.NoRelatedTypeObject no longer runs under IFC2X3, which does not declare it. Unsupported rules are likewise admitted only under releases that declare them. A new schema-backed test checks every registered id, entity and release set against the normative EXPRESS.
  • References and values inside aggregates and SELECT slots are type-checked (#113). structure.reference.wrong_type now judges every member of an aggregate of entities (a property set in SET OF IfcProduct), including aggregates reached through a type that aliases one; type.select.member now judges an entity reference against the SELECT's closure, directly and inside aggregates (an IfcWall as RelatingMaterial); and type.scalar.mismatch now judges aggregate members (a string in Coordinates) and a reference in a primitive slot. Nested attribute aggregates (LIST OF LIST OF ...) stay unchecked: the schema tables do not retain their element type. A reference to an entity whose type the tables do not declare -- typically a later release's entity -- is no longer reported as structure.reference.wrong_type; there is no basis for a subtype verdict, and type.entity.unknown already warns about it.
  • type.scalar.mismatch now checks bounded and fixed-width strings. The primitive was read from the trailing token of the resolved type, so STRING(255) -- IFC4's IfcLabel and IfcIdentifier -- recognised nothing and every such slot went unchecked; an integer in a Name slot was accepted.

openbim-ifc ​

Added ​

  • ids_of_type_including_subtypes (feature schema): every entity of a type or any of its subtypes, in file order. Model::ids_of_type("IfcElement") returns nothing because no instance is declared as the abstract supertype; this answers the question that call looks like it should. The caller passes the Schema, because the tree differs by version (#32).

Changed ​

  • Breaking: requires ifc-style 0.3.0, re-exported as ifc::style. IndexedTextureMap::maps there now returns Vec<EntityId>.

openbim-ifc-cli ​

Added (#464) ​

  • select FILE QUERY [--value PATH]... [--format table|json|csv] lists the entities a selector selects, in ascending id order, with their class, GlobalId, Name and one column per value path. JSON carries the query in canonical form and the grammar version. A selector or path that does not parse, or names a class or attribute the declared release lacks, is refused with the new kind invalid-selector and its byte span (exit 2).

Semver: minor (additive).

[0.2.4] - 2026-09-28 ​

ifc-schema ​

Maintenance release from maint/ifc-schema-0.2; the code is unchanged since 0.2.3.

Added ​

  • A crate README, which is the crates.io page.

ifc-spatial ​

Added ​

  • A *_with_owner_history variant of every IfcRoot writer (#202): create_spatial_element, create_project, aggregate, contain, create_external_spatial_element, create_project_library, create_space_boundary, connect_path_elements, create_facility, and in authoring cover_elements, cover_spaces, declare, define_by_object, serve_buildings, control_flow_element, assign_to_actor, assign_to_product, assign_to_process, assign_to_group_by_factor, assign_to_resource, connect_elements, connect_with_realizing_elements, interfere_elements, void_element, fill_element, project_element, adhere_to_element, position_products and associate_profile_def. Each takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot; the id must be in the model or staged on the transaction and be an IfcOwnerHistory, and none is ever invented. Each binds the model's declared release as ifc-material (#77), ifc-properties (#191) and ifc-classification (#194) do (no FILE_SCHEMA binds IFC4) and lays the record out by attribute name from that release's table: IFC2X3 IfcRelCoversSpaces.RelatingSpace is written as RelatedSpace, IFC4's name for the same attribute. What the release cannot hold is refused: entities it does not declare (IfcRelDeclares, IfcRelDefinesByObject, IfcRelAssignsToGroupByFactor, IfcRelInterferesElements, IfcExternalSpatialElement, IfcProjectLibrary and the 1st- and 2nd-level space boundaries in IFC2X3; IfcRelAdheresToElement, IfcRelPositions, IfcRelAssociatesProfileDef and the facilities outside IFC4X3), enumeration tokens outside its table, and attributes it requires but the call cannot supply (IFC2X3 CompositionType, IfcSpace.InteriorOrExteriorSpace, IfcProject.RepresentationContexts). In IFC4 and IFC4X3 each variant writes the plain writer's record with the reference in slot 1.
  • SpatialAuthoringError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringValueType, AuthoringRequired, MissingReference and WrongReferenceType, and FacilityError::Authoring wrapping them (#202). Both enums are #[non_exhaustive], so this is not breaking.

Changed ​

  • create_space_boundary binds the model's declared release and lays its record out by attribute name (#202). It wrote the IFC4 layout with OwnerHistory $ into every model, and IFC2X3 requires the owner history. Behaviour change for IFC2X3 callers: it now refuses an IFC2X3 model with SpatialAuthoringError::AuthoringRequired { attribute: "OwnerHistory", .. } and stages nothing; use create_space_boundary_with_owner_history. It also refuses a header declaring several schemas (MultipleSchemas) or one without a bundled table (UnsupportedSchema), an IFC2X3 1st- or 2nd-level boundary (EntityNotInSchema) and a token the release does not declare, such as IFC2X3 EXTERNAL_EARTH (AuthoringValueType). IFC4 and IFC4X3 records are unchanged, slot for slot.

Documented ​

  • The writers that take no model cannot see the release: they write the IFC4/IFC4X3 layout with OwnerHistory $ and are documented as IFC4/IFC4X3 only, pointing to their *_with_owner_history variant. Their output is unchanged. Four of them disagree with the release's arity, which the tests pin and the variants do not repeat: assign_to_actor and assign_to_process write seven of eight attributes, connect_with_realizing_elements eight of nine, and interfere_elements ten where IFC4 declares nine.

[0.2.3] - 2026-09-28 ​

ifc-cost ​

Added ​

  • create_cost_item_with_owner_history, create_cost_schedule_with_owner_history, nest_cost_items_with_owner_history and assign_schedule_items_with_owner_history (#202). Each takes a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. It must be in the model or staged on the transaction (MissingReference otherwise) and be an IfcOwnerHistory (WrongReferenceType). None is ever invented. In IFC4 and IFC4X3 the record is the plain writer's with the reference in the optional slot.
  • CostAuthoringError::AuthoringValueType and CostAuthoringError::AuthoringRequired, appended to the #[non_exhaustive] enum, so not breaking.

Changed ​

  • create_cost_item, create_cost_schedule, nest_cost_items and assign_schedule_items bind the model's declared release and lay their records out by attribute name from its table (#202), as quantity authoring already did. They wrote the IFC4 layout with OwnerHistory $ into every model, which is invalid IFC2X3. Behaviour change for IFC2X3 callers: they now refuse an IFC2X3 model with AuthoringRequired { attribute: "OwnerHistory", .. } and stage nothing; use the *_with_owner_history variants there. What IFC2X3 cannot hold is refused, never dropped: an Identification, PredefinedType or cost values on a cost item (AuthoringNotInSchema), a date string where it declares an IfcDateTimeSelect (AuthoringValueType), and a cost schedule without its required ID (written from identification) or PredefinedType (AuthoringRequired). A header declaring several schemas, or one without a bundled table, is refused with MultipleSchemas or UnsupportedSchema. A model without FILE_SCHEMA binds IFC4 as before. IFC4 and IFC4X3 records are unchanged, record for record.

Fixed ​

  • assign_cost_quantities accepts every instantiable subtype of the release's own CostQuantities declaration (IfcPhysicalQuantity), read from its table instead of a fixed list (#203). An IFC4X3 IfcQuantityNumber was refused. IFC4 still refuses it, as IFC4 does not declare it. The slot is found by name, and an IFC2X3 cost item, which has no CostQuantities, is refused with AuthoringNotInSchema instead of written past its five attributes. A header that binds no single known release is refused as above.

ifc-model ​

Added ​

  • Lazily decoded entities (ADR 0015). A codec builds a model with Model::with_source(Arc<dyn EntitySource>) and registers each entity as a byte span plus type name with Model::insert_lazy; Model::get decodes an entity on first access and keeps it, so references stay stable and every later access is a lookup. Type queries, ids, len, contains and next_id need no decoding. Model::decode_all(threads) decodes the rest in parallel for a consumer about to touch everything; Model::decoded_len reports progress. Editing, cloning and removal decode first and then behave exactly as before; clones share the source.
  • Codec::read_owned(Vec<u8>): read from a buffer the model may keep. Defaults to read_bytes; read_from now hands its buffer over.

Fixed ​

  • Guid::parse rejects a GlobalId whose leading character is not 0–3 (#62). 22 base-64 digits carry 132 bits and a UUID has 128, so a higher leading digit names no UUID. Before, it was accepted and to_uuid dropped the extra high bits: 0000000000000000000000 and 4000000000000000000000 expanded to the same UUID, and $$$$… came back from a round trip as 3$$$…. Every accepted Guid now round-trips through to_uuid/from_uuid unchanged.
  • Authoring helpers that validate through Guid::parse (in ifc-spatial, ifc-systems, ifc-structural and others) now refuse such ids as well. ifc-resource already did. A file that was written with one would have failed its own GlobalId check.

ifc-schema ​

Maintenance release from maint/ifc-schema-0.2: the 0.2.2 code with only the data change below, so it keeps openbim-step 0.5.1 and every dependent's ^0.2.2 resolves to it. main carries the same change.

Changed ​

  • The bundled IFC2X3 TC1, IFC4 ADD2 TC1 and IFC4X3 ADD2 schemas no longer carry WHERE-rule expressions: EntityDef::where_rules keeps each rule's label, and its expression is empty. The expressions are CC BY-ND schema text that nothing here evaluates. A schema parsed at run time with Schema::from_express keeps them. The artifacts are about 23-38% smaller. data/NOTICE.md now states the provenance of the bundled files.

ifc-spatial ​

Added ​

  • SpatialTree::referenced_elements(container) and SpatialTree::referencing_structures(element) (#121): the elements an IfcRelReferencedInSpatialStructure references in a container, and the containers referencing an element, in file order and each once, in IFC2X3, IFC4 and IFC4X3. They are kept apart from containment: elements_of and container_of are unchanged, and a referenced element is never a second home or a ContainedTwice anomaly. A reference naming an absent entity is reported by dangling().
  • SpatialAnomaly::ContainedInNonContainer and SpatialAnomaly::ReferencedInNonContainer (#121): an IfcRelContainedInSpatialStructure or IfcRelReferencedInSpatialStructure whose RelatingStructure is not a spatial container of the release is reported with the relationship and the structure, where containment used to drop it silently.
  • SpatialKind::classify_in(type_name, release), SpatialTree::release() and a re-export of ifc_schema::SchemaVersion.

Changed ​

  • Spatial containers are classified from the release the file's FILE_SCHEMA declares (#121): an entity is a container when that release's bundled table makes it an IfcSpatialElement (IFC2X3: IfcSpatialStructureElement), or it is the IfcProject. ifc-schema is therefore a normal dependency. The IFC4X3 facilities and facility parts (IfcFacility, IfcBridge, IfcRoad, IfcRailway, IfcMarineFacility, IfcBridgePart, IfcRoadPart, IfcRailwayPart, IfcMarinePart, IfcFacilityPartCommon) and IfcExternalSpatialElement were classified as elements by the old name patterns, so containment into them was dropped; they are now OtherContainer, as that variant's documentation promised. SpatialKind gains no variant, so this stays additive. A file with no single bundled release is classified as any bundled release would, and release() returns None.
  • SpatialKind::classify answers from the bundled tables instead of name patterns: a name no release declares as a spatial element (such as a vendor IFCSPATIALFOO) is an Element.

Fixed ​

  • Only IfcRelAggregates and IfcRelContainedInSpatialStructure build the tree (#121). Another relationship family whose relating end is a container placed its targets as contained elements: an IfcRelDeclares put the project's declared types into the project, and an IfcRelCoversSpaces put a space's coverings into the space. Their absent targets are still reported by dangling().

ifc-systems ​

Added ​

  • A release-bound *_with_owner_history variant of every systems writer (#202): create_system_with_owner_history, create_port_with_owner_history, assign_to_group_with_owner_history, nest_ports_with_owner_history, connect_port_to_element_with_owner_history, connect_ports_with_owner_history, contain_in_spatial_structure_with_owner_history, reference_in_spatial_structure_with_owner_history, create_group_with_owner_history, authoring::create_distribution_element_with_owner_history, authoring::create_zone_with_owner_history, authoring::create_spatial_zone_with_owner_history and create_classified_system_with_owner_history. Each takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and be an IfcOwnerHistory; none is ever invented. Each binds the model's declared release (a header without FILE_SCHEMA binds IFC4, as the other crates do), runs the plain writer's checks, and lays the record out by attribute name from that release's table. So an IFC2X3 IfcDistributionPort has eight attributes and an IFC2X3 IfcZone five. Enumeration tokens are checked against the release's own table (IFC4 refuses the IFC4X3 IfcSpatialZoneTypeEnum tokens). In IFC4 and IFC4X3 a variant writes the plain writer's record with the reference in the optional slot. create_classified_system_with_owner_history binds the model's release instead of taking a schema argument.
  • SystemAuthoringError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringValueType, AuthoringRequired, MissingReference and WrongReferenceType, for the variants' refusals: a header binding no single known release, an entity or attribute the release does not declare (IFC2X3 IfcSpatialZone, IfcZone.LongName; IfcBuiltSystem outside IFC4X3), a value it cannot hold, a required attribute left unset, and a missing or wrong-type owner history. SystemAuthoringError is #[non_exhaustive] and the variants are appended, so this is not breaking. Nothing is staged on a refusal.

Documented ​

  • The plain writers take no model, so they cannot see the release: they still write the IFC4/IFC4X3 layout with OwnerHistory $, unchanged, and are documented as IFC4/IFC4X3 only, pointing to their variant. IFC2X3 declares none of the classified-system entities, so create_classified_system never wrote an IFC2X3 record.

[0.2.2] - 2026-09-28 ​

ifc-author ​

Added ​

  • AuthorError::ValueForm: a value of the declared type written in the form ISO 10303-21 does not use for it (#199). A typed parameter (IFCAREAMEASURE(12.5)) is refused where the declared type is not a SELECT, and a bare value where it is one, for scalars and for each member of an aggregate, in EntityBuilder and EntityEditor.

Changed ​

  • EntityEditor re-checks every slot of the projected entity, as before, so editing an entity whose untouched slots already hold a value in the wrong form is now refused with ValueForm until that slot is rewritten.

Fixed ​

  • A typed wrapper was judged against its own type only, so any wrapper passed in a slot whose declared type is not a SELECT (#199). IFCLABEL('x') in IfcQuantityArea.AreaValue, or in an entity-typed slot, is now a TypeMismatch, and so is a wrapper naming a type outside a SELECT's select-list, nested SELECTs included. A declared type the tables cannot resolve still accepts either form.

ifc-classification ​

Changed (breaking behaviour) ​

The public API only grows (cargo semver-checks against 0.2.1 reports no break), but the same call now answers differently in the cases below, so the next release is 0.3.0. Each change is required by #194: an IFC4X3 model must be read against its own table, and a header that binds no single known release must be refused rather than read as IFC4.

  • Views and authoring bind the release the header declares (#194), as ifc-material does since #77. One IFC2X3, IFC4, or IFC4X3/IFC4X3_ADD2 declaration binds that release's bundled table (IFC2X3 TC1, IFC4 ADD2 TC1, IFC4X3 ADD2). Before, every header other than IFC2X3 was read against IFC4, so a consumer had to refuse an IFC4X3 model's classifications.
    • classification_schema answers SchemaVersion::Ifc4x3 for an IFC4X3 header, where it answered Ifc4.
    • An element that exists only in IFC4X3 (for example IfcRoad) is an IfcDefinitionSelect member there, so its classification resolves; read against IFC4 it was a ReferenceType error.
    • IfcResourceObjectSelect is the IFC4X3 one, which adds IfcShapeAspect, for external_reference_relationship(s), external_references_for and create_external_reference_relationship.
    • IFC4X3 renamed IfcClassification.Location to Specification, same position and type. ClassificationSystem::location() and ClassificationDraft::location read and write it. No other attribute this crate touches differs between IFC4 and IFC4X3 (IfcClassificationReference, IfcRelAssociatesClassification, IfcExternalReference.Identification, documents, libraries and IfcRoot are unchanged).
  • A header declaring several schemas fails every read and write with MultipleSchemas; before, only a set including IFC2X3 did, and e.g. IFC4+IFC4X3 was read as IFC4. One declaration without a bundled table (such as IFC4X1) fails with the new UnsupportedSchema; before, it was read as IFC4. A header with no declaration (an in-memory model) still binds IFC4, as in 0.2.0.
  • Authoring lays each record out by attribute name in the declared release's table instead of writing the IFC4 layout into every model. IFC2X3 gets its own records (a four-attribute IfcClassification, ItemReference/DocumentId for Identification, a three-attribute IfcDocumentInformationRelationship, ...). What IFC2X3 cannot hold is refused with a typed error and nothing is staged:
    • a draft value for an attribute it lacks (IfcClassificationDescription, Location, ReferenceTokens; a reference's Description or Sort; a document's Location; a document reference's Description or ReferencedDocument; a library's Location or Description; a library reference's Description, Language or ReferencedLibrary) with AuthoringNotInSchema;
    • text for an attribute it types as a record (EditionDate, CreationTime, LastRevisionTime, ElectronicFormat, ValidFrom, ValidUntil, VersionDate) with AuthoringValueType;
    • a required attribute left unset (IfcClassification.Source and Edition, and IfcRoot.OwnerHistory) with AuthoringRequired. So associate_classification, associate_document and associate_library refuse an IFC2X3 model; use their new *_with_owner_history variants there;
    • a reference its selects do not admit (an IfcClassification as RelatingClassification, which IFC2X3 types as IfcClassificationNotationSelect; a reference as ReferencedSource; a person as library Publisher) with AuthoringReferenceType;
    • create_external_reference_relationship with EntityNotInSchema. IFC4 output is unchanged, and IFC4X3 output is the IFC4 record at the same positions.
  • Every reference an authoring call checks is checked against the type the release declares, and the error's expected label is that type's name (the same strings as before for IFC4).

Added ​

  • create_classification_in(tx, model, draft): create_classification in the model's release. create_classification takes no model, so it keeps writing the IFC4 layout (valid for IFC4 and IFC4X3) and is documented as such.
  • associate_classification_with_owner_history, associate_document_with_owner_history and associate_library_with_owner_history: take a caller-supplied IfcOwnerHistory, which IFC2X3 requires. It must be in the model or staged, and be an IfcOwnerHistory; one is never invented. This follows ifc-material (#77) and ifc-properties (#191).
  • ClassificationError::UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringRequired and AuthoringValueType. ClassificationError is #[non_exhaustive], so these are additive.

Fixed ​

  • DocumentInformation::status() and create_document accept FINALDRAFT. Every release declares it in IfcDocumentStatusEnum, but both hard-coded a list without it, so a valid document was InvalidValue. Both enumerations now come from the release's table.
  • effective_classifications reads IfcRelDefinesByType by attribute name instead of fixed positions.

Verified ​

  • Every attribute, select and enumeration the crate touches is pinned against the three bundled tables, and the tables against the normative EXPRESS under references/ifc-spec (tests/release_layout.rs).
  • Records authored in IFC2X3, IFC4 and IFC4X3 through every writer are written as STEP, read back, re-read through the views, and validated by ifc-validate with no error finding (openbim-ifc/tests/release_bound_classification_authoring.rs).

ifc-control ​

Added ​

  • create_control_with_owner_history and assign_to_control_with_owner_history (#198, #202). Each binds the model's declared release (a header without FILE_SCHEMA binds IFC4) and takes a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. It must be in the model or staged on the transaction (UnknownEntity otherwise) and be an IfcOwnerHistory (AuthoringInvalid). None is ever invented. In IFC4 and IFC4X3 the record is the plain writer's with the reference in the optional slot.
  • ControlError::MultipleSchemas, UnsupportedSchema, AuthoringNotInSchema, AuthoringValueType and AuthoringRequired, appended to the #[non_exhaustive] enum, so not breaking.

Fixed ​

  • create_control no longer panics on an IFC2X3 IfcPermit, IfcActionRequest or IfcPerformanceHistory (#198). It indexed IFC4 positions into their six-attribute IFC2X3 records. create_control and assign_to_control now lay records out by attribute name from the table they are given, so a value that table does not declare is refused (AuthoringNotInSchema: an IFC2X3 permit has no PredefinedType, Status or LongDescription), one it cannot hold is refused (AuthoringValueType), and a required one left unset is refused (AuthoringRequired). In IFC2X3, identification is written as the entity's own identifier (PermitID, RequestID, ID), which the IFC4 documentation records as renamed to Identification. Behaviour change for IFC2X3 callers: IfcRoot.OwnerHistory is mandatory there, so both plain writers refuse an IFC2X3 schema with AuthoringRequired { attribute: "OwnerHistory", .. } and stage nothing (an IFC2X3 project order used to be written with $); use the *_with_owner_history variants. IFC4 and IFC4X3 records are unchanged, record for record.

ifc-cost ​

Added ​

  • CostAuthoringError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema and AuthoringNotInSchema for release-bound quantity authoring (#190), and a re-export of SchemaVersion, which they name. CostAuthoringError is #[non_exhaustive], so this is not breaking. The crate now depends on ifc-schema for the bundled release tables.

Fixed ​

  • mutation::create_quantity writes the quantity value bare (#190). It wrote IFCQUANTITYAREA('Q',$,$,IFCAREAMEASURE(12.5),$). But <Kind>Value is declared with a defined measure type, not a SELECT, in IFC2X3, IFC4 and IFC4X3, and ISO 10303-21 writes a typed parameter only in a SELECT slot. It now writes 12.5, and a count as the integer 4. CostQuantity::value reads both forms.

  • mutation::create_quantity binds the model's declared release, with the same rule and output as ifc-properties' create_quantity (#190). It wrote the IFC4 five-attribute layout into every model. Now attributes are placed by name from the release's table, so an IFC2X3 quantity has four. The signature is unchanged; the refusals are new:

    • a formula in IFC2X3 (AuthoringNotInSchema);
    • QuantityKind::Number outside IFC4X3 (EntityNotInSchema);
    • a header declaring several schemas (MultipleSchemas);
    • a header declaring one without a bundled table (UnsupportedSchema).

    A model without FILE_SCHEMA binds IFC4 as before. Behaviour change: such a model now refuses QuantityKind::Number, which IFC4 does not declare; declare IFC4X3 to author one. IFC4 and IFC4X3 records are otherwise unchanged apart from the bare value, and match ifc-properties byte for byte in all three releases.

ifc-model ​

Fixed ​

  • Builds for wasm32-unknown-unknown (#34). ahash's default runtime-rng pulled in getrandom 0.3, which fails on that target, so no crate depending on this one could be compiled to WebAssembly. Native builds keep runtime-seeded hashing; wasm32 builds use a compile-time seed.

ifc-schema ​

Fixed ​

  • Builds for wasm32-unknown-unknown (#34). ahash's default runtime-rng pulled in getrandom 0.3, which fails on that target, so no crate depending on this one could be compiled to WebAssembly. Native builds keep runtime-seeded hashing; wasm32 builds use a compile-time seed.

ifc-spatial ​

Added ​

  • SpaceBoundary::connection_geometry(&Model) and ConnectionGeometryAnomaly (#156). The accessor returns the ConnectionGeometry reference (slot 6) of IfcRelSpaceBoundary, IfcRelSpaceBoundary1stLevel and IfcRelSpaceBoundary2ndLevel, in IFC2x3, IFC4 and IFC4X3. Its coordinates are in the relating space's object placement. $ or a missing slot is Ok(None). A dangling reference, a reference to something that is not a concrete IfcConnectionGeometry subtype, a value that is not a reference, and a boundary absent from the model are each an Err naming the boundary and, where there is one, the target. The accessor is a method rather than a new field so that SpaceBoundary, which has only public fields, keeps its struct-literal construction and this change stays additive.

ifc-systems ​

Fixed ​

  • The zone readers bind an IFC4X3 header to the bundled IFC4X3 ADD2 table (#194). Before, zones() read IFC4X3 against IFC4 and long_name_of refused it with UnsupportedSchema. Now:
    • zones() reads IFC4X3 against its own table, and reads every slot by attribute name (IfcZone.Name/LongName, IfcRelAssignsToGroup.RelatedObjects/RelatingGroup) instead of fixed positions. IFC4X3 retypes IfcRelAssigns.RelatedObjectsType as IfcStrippedOptional at the same position, adds IfcBuiltSystem under IfcSystem (no WR1 zone member), and leaves IfcZone as in IFC4, so IFC4-shaped data gives the same zones as before;
    • long_name_of answers for an IFC4X3 model instead of refusing it.

Added ​

  • try_zones(model): zones() with an error channel. A header declaring several schemas is refused with SchemaResolutionError::MultipleSchemas and one without a bundled table with UnsupportedSchema, where zones() reads both against IFC4. A header with no schema (an in-memory model) reads against IFC4, as zones() does.

Unchanged ​

  • zones() keeps its signature and its fallback: a header it cannot bind (none, several, or unknown) reads against IFC4, as in 0.2.0.
  • schema_of, systems, ports, ElementRole::of and ConnectionGraph::build still treat IFC4X3 as unverified: schema_of refuses it and the bulk readers read it against IFC4. Only the zone readers are verified for IFC4X3.
  • The Display text of SchemaResolutionError::UnsupportedSchema no longer claims only IFC2X3 and IFC4 resolve, since the zone readers resolve IFC4X3.

[0.2.1] - 2026-09-28 ​

ifc-approval ​

Added ​

  • associate_approval_with_owner_history (#202). It takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and must be an IfcOwnerHistory: a missing one is refused with UnknownEntity, another entity with AuthoringReferenceType. None is ever invented. In IFC4 and IFC4X3 the reference fills the optional slot. This follows ifc-material's associate_material_with_owner_history (#77) and ifc-properties' *_with_owner_history writers (#191).
  • ApprovalError::MultipleSchemas, ApprovalError::UnsupportedSchema and ApprovalError::AuthoringRequired { entity, attribute, schema } (#202). ApprovalError is #[non_exhaustive], so this is not breaking.

Fixed ​

  • associate_approval no longer writes IfcRelAssociatesApproval with OwnerHistory $ into an IFC2X3 model, where it is mandatory (#202). It binds the model's declared release (a header without FILE_SCHEMA binds IFC4), lays the record out by attribute name from that release's table, and checks RelatedObjects against it: IfcDefinitionSelect in IFC4 and IFC4X3, IfcRoot restricted by WR21 (object and property definitions) in IFC2X3. IFC4 and IFC4X3 output is unchanged. Behaviour change: an IFC2X3 model is refused with AuthoringRequired { attribute: "OwnerHistory", .. } and nothing is staged; use associate_approval_with_owner_history there. A header that declares several schemas, or one without a bundled table, is refused with MultipleSchemas or UnsupportedSchema where it was written before. The other writers of this crate carry no IfcRoot and are unchanged.

ifc-author ​

Fixed ​

  • An attribute declared as a defined type that aliases an aggregate is an aggregate (#17). IfcSite.RefLatitude/RefLongitude (IfcCompoundPlaneAngleMeasure = LIST [3:4] OF INTEGER) were refused with AggregateMismatch, which blocked georeferencing. Their elements are now checked against the alias's element type.
  • A slot that the entity or a supertype redeclares as DERIVE is written * automatically (#18). IfcSIUnit.Dimensions and the four derived slots of IfcGeometricRepresentationSubContext reported MissingRequired, so no unit assignment or Body/Axis subcontext could be authored. Passing Value::Derived explicitly is also accepted.

Added ​

  • AuthorError::DerivedAttribute refuses a value or $ in a derived slot. AuthorError::NotDerived refuses * in a slot the schema does not derive. Before, * was accepted in any slot and the file was invalid. Both apply to EntityBuilder and EntityEditor.

ifc-classification ​

Added ​

  • Views read IFC2X3 models against the IFC2X3 schema (#51). A header declaring exactly IFC2X3 binds the IFC2X3 table; every slot position, select, and domain comes from it. Before, every model was read with IFC4 positions, so IFC2X3 files only worked where the two releases happen to share a slot.
    • IfcRelAssociatesClassification.RelatingClassification accepts IFC2X3 IfcClassificationNotationSelect (notation or reference), and RelatedObjects is checked against IFC2X3 IfcRoot.
    • IFC2X3 ItemReference and DocumentId are read by the IFC4-named identification() accessors, since they keep position and meaning.
    • IfcClassificationReference.ReferencedSource follows the declared release: IfcClassification only in IFC2X3.
  • classification_schema(model) reports the release reads bind to. SchemaVersion is re-exported.
  • ClassificationNotation, ClassificationView::notations(), notation(), and notation_values(): an IFC2X3 notation's code is its facets' NotationValues, in authored order, with no invented separator.
  • ClassificationError::NotInSchema: an accessor for an attribute the release lacks (for example description() on an IFC2X3 IfcClassification, or any IfcExternalReferenceRelationship read in an IFC2X3 model) fails with this instead of returning Ok(None), which would read as "authored as empty".
  • ClassificationError::StructuredValue: a text accessor that meets an attribute the release types as a record, such as an IFC2X3 IfcCalendarDate EditionDate, returns the record id instead of rejecting a valid value as InvalidValue.
  • ClassificationError::MultipleSchemas: a header declaring several schemas including IFC2X3 cannot be bound to one release.

Unchanged ​

  • IFC4 and undeclared (in-memory) models read exactly as in 0.2.0: every existing IFC4 test passes untouched. On two real IFC4 models, all 1,482 classified objects resolve.
  • IFC4X3 headers still read against IFC4, as in 0.2.0. Their own table is out of scope here.
  • Authoring stays IFC4-only.
  • Projections built with try_new have no model header, so they keep reading against IFC4.

Verified ​

  • On five real IFC2X3 models (two ArchiCAD, Solibri, a structural model, Revit), all 3,672 classified objects resolve their effective classifications, and every classification system (5) and reference (62) reads, IFC2X3 calendar edition dates included.

ifc-constraint ​

Added ​

  • associate_constraint_with_owner_history (#202). It takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and must be an IfcOwnerHistory: a missing one is refused with UnknownEntity, another entity with AuthoringReferenceType. None is ever invented. In IFC4 and IFC4X3 the reference fills the optional slot. This follows ifc-material (#77) and ifc-properties (#191).
  • ConstraintError::MultipleSchemas, ConstraintError::UnsupportedSchema and ConstraintError::AuthoringRequired { entity, attribute, schema } (#202). ConstraintError is #[non_exhaustive], so this is not breaking.

Fixed ​

  • associate_constraint no longer writes IfcRelAssociatesConstraint with OwnerHistory $ into an IFC2X3 model, where it is mandatory (#202). It binds the model's declared release (a header without FILE_SCHEMA binds IFC4), lays the record out by attribute name from that release's table, and checks RelatedObjects against it (IfcDefinitionSelect in IFC4 and IFC4X3, IfcRoot restricted by WR21 in IFC2X3). IFC4 and IFC4X3 output is unchanged. Behaviour change: an IFC2X3 model is refused with AuthoringRequired { attribute: "OwnerHistory", .. } and nothing is staged; use associate_constraint_with_owner_history there. IFC2X3 also requires Intent, which IFC4 made optional, so an IFC2X3 association without one is refused with AuthoringRequired on Intent. A header that declares several schemas, or one without a bundled table, is refused with MultipleSchemas or UnsupportedSchema. The other writers carry no IfcRoot and are unchanged.

ifc-control ​

Added ​

  • assign_to_control and ControlAssignmentDraft stage an IfcRelAssignsToControl whose relating control is a permit, project order, action request or performance history. Empty, duplicated and self-referencing RelatedObjects, members that are not IfcObjectDefinitions, and missing references are refused; RelatedObjectsType is left unset (#99).
  • ControlError::ForeignControl refuses a relating control another crate owns (cost schedules, cost items, work controls).
  • ControlKind::ALL lists the four owned controls.

ifc-cost ​

Added ​

  • nesting_anomalies(model) and CostAnomaly::NestedTwice { item, kept, rejected, relation } (#57). A cost item that two IfcRelNests place under different parents is reported; Nests is SET [0:1].

Fixed ​

  • A cost item nested under two parents is no longer counted twice (#57). parent_of already returned the first parent, but children_of listed the item under both. So descendants_of and rolled_up_total included it under each parent, and summing over roots() double-counted its value. Now only the kept parent (first IfcRelNests in file order) lists it, and a child listed twice under one parent is listed once. Output changes only for files that violate the schema.

ifc-element-type ​

Added ​

  • create_type_in and create_supertype_in (#202): create_type and create_supertype in the model's declared release. They bind it (a header without FILE_SCHEMA binds IFC4), lay the record out by attribute name from its table and check predefined_type against its enumeration. A type the release does not declare, or declares abstract, is refused with EntityNotInSchema (IFC2X3 has no IfcDoorType and no resource or process types; IFC4 has no IfcBearingType or IfcBuiltElementType). A token where the release declares no PredefinedType is refused with AuthoringNotInSchema. OwnerHistory is left $, which IFC2X3 forbids, so an IFC2X3 model is refused with AuthoringRequired.
  • create_type_with_owner_history and create_supertype_with_owner_history (#202): the same, with a caller-supplied IfcOwnerHistory, which IFC2X3 requires. It must be in the model or staged on the transaction (MissingEntity otherwise) and be an IfcOwnerHistory (Invalid on OwnerHistory otherwise). None is ever invented. This follows ifc-material (#77) and ifc-properties (#191).
  • ElementTypeError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringRequired and MissingEntity (#202), appended. ElementTypeError is #[non_exhaustive], so this is not breaking; its Display covers them.
  • ifc-schema is now a dependency (it was a dev-dependency).

Changed ​

  • create_type and create_supertype are unchanged: they take no model and still write the catalogue's IFC4X3 layout with OwnerHistory $. That is never valid IFC2X3, and they cannot refuse a model that declares another release; their documentation now says so. They also still write $ into the four required attributes TypeDraft has no field for (IfcDoorType.OperationType, IfcWindowType.PartitioningType, IfcEventType.EventTriggerType, IfcFurnitureType.AssemblyPlace), where the model-bound writers refuse with AuthoringRequired.
  • ElementTypeError moved to its own module; the public path ifc_element_type::ElementTypeError is unchanged.

ifc-model ​

Changed ​

  • Model::ids_of_type docs now point to ifc::ids_of_type_including_subtypes for subtype-inclusive queries, instead of wrongly saying ifc-schema provides them.

ifc-occurrence ​

Added ​

  • create_with_owner_history (#202). It takes a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and must be an IfcOwnerHistory: a missing one is refused with OccurrenceError::UnresolvedOwnerHistory, another entity with OccurrenceError::NotAnOwnerHistory. None is ever invented. This follows ifc-material (#77) and ifc-properties (#191).
  • OccurrenceError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringRequired, UnresolvedOwnerHistory and NotAnOwnerHistory (#202), appended. OccurrenceError is #[non_exhaustive], so this is not breaking.
  • ifc-schema is now a dependency: the declared release's table lays out each record.

Fixed ​

  • create writes the model's declared release instead of the IFC4X3 catalogue row's layout (#202). It wrote OwnerHistory $, mandatory in IFC2X3, and the IFC4X3 arity, PredefinedType slot and tokens into every model. It now binds the release (a header without FILE_SCHEMA binds IFC4), lays the record out by attribute name from that release's table, and checks predefined_type against that release's enumeration. IFC4X3 output is unchanged for every catalogue row, and IFC4 output for every row IFC4 declares with the same layout (both are tested). Behaviour changes: an IFC2X3 model is refused with AuthoringRequired { attribute: "OwnerHistory", .. }; use create_with_owner_history there. A class the bound release does not declare is refused with EntityNotInSchema: IFC4X3-only classes such as IfcBorehole are refused in an IFC4 model and in a model whose header declares no schema, which binds IFC4; declare IFC4X3 (or IFC4X3_ADD2) to author them. A token only IFC4X3 declares is refused in IFC4 with UnknownPredefinedType, and IFC2X3 classes without a PredefinedType (IfcDoor, IfcWall) refuse one with NoPredefinedType. A required attribute the draft cannot carry is refused with AuthoringRequired, not written $: in IFC2X3, IfcRamp/IfcRoof/IfcStair.ShapeType, IfcReinforcingBar/IfcTendon.NominalDiameter and IfcReinforcingMesh.LongitudinalBarNominalDiameter. Several or unknown FILE_SCHEMA tokens are refused with MultipleSchemas or UnsupportedSchema. The type pairing is still the IFC4X3 CorrectTypeAssigned class in every release, so an IFC2X3 IfcDoorStyle is not accepted as typed_by.

ifc-properties ​

Added ​

  • exact_property resolves IFC2X3 models (#48). It binds to the release the single FILE_SCHEMA token declares, IFC2X3 TC1 or IFC4 ADD2 TC1, and reads every structural fact from that release's bundled table: slot counts, entity domains (IFC2X3 RelatedObjects is IfcObject, IFC4's is IfcObjectDefinition), type objects (IFC2X3 IfcDoorStyle and IfcWindowStyle carry inherited properties like any other type), and the IfcValue/IfcUnit selects. An 8-slot IFC2X3 IfcWall resolves under an IFC2X3 header and is a slot mismatch under IFC4, and the reverse.
  • exact_schema(model) reports the release a resolution binds to, so a consumer can bind vocabulary per release without re-parsing the header. It fails exactly where exact_property fails at model level. SchemaVersion is re-exported.
  • ExactPropertyError::NotInSchema names a construct the declared release does not define, such as an IfcDoorType, an IfcPropertySetDefinitionSet, or an IFCBINARY value in an IFC2X3 file. A file that mixes releases fails closed instead of being read with the wrong table.
  • ExactPropertyError::UnsupportedRelationship: a proper subtype of IfcRelDefinesByProperties or IfcRelDefinesByType that relates the queried object, such as IFC2X3 IfcRelOverridesProperties, is refused. Before, it was skipped as if absent, and the overridden value was returned.

Changed ​

  • IFC4X3 headers stay UnsupportedSchema: the table is bundled, but exact semantics for it are not verified yet (#33 tracks IFC4X1/IFC4X2).
  • On five real IFC2X3 models, every sampled lookup failed with UnsupportedSchema before. Now 715 of 726 sampled lookups resolve; the other 11 are DuplicateMatchingSets, where a property asked for by name alone is carried by two sets assigned to the same object. The same refusal occurs on the IFC4 control models.

ifc-schedule ​

Added ​

  • create_task_with_owner_history, create_sequence_with_owner_history, create_work_control_with_owner_history, assign_tasks_to_control_with_owner_history, nest_tasks_with_owner_history, create_work_calendar_with_owner_history, create_event_with_owner_history and create_procedure_with_owner_history (#202). Each takes the model and a caller-supplied IfcOwnerHistory id, which IFC2X3 requires on every IfcRoot. The id must be in the model or staged on the transaction and must be an IfcOwnerHistory; a missing one is refused with MissingReference, another entity with WrongReferenceType. None is ever invented. Each binds the model's declared release (a header without FILE_SCHEMA binds IFC4; several or an unknown token are refused) and lays the record out by attribute name from that release's table. In IFC4 and IFC4X3 the record is the plain writer's with the reference in the optional slot. This follows ifc-material (#77), ifc-properties (#191) and ifc-control (#198).
  • In IFC2X3 the variants write Identification as IfcTask.TaskId and IfcProcedure.ProcedureID, and PredefinedType as IfcProcedure.ProcedureType (renames stated in the IFC4 ADD2 TC1 documentation). What IFC2X3 cannot hold is refused, never dropped: IfcEvent and IfcWorkCalendar (EntityNotInSchema); a task's LongDescription, TaskTime or PredefinedType (AuthoringNotInSchema); a sequence's IfcLagTime reference where IFC2X3 declares an IfcTimeMeasure, and a work control's ISO 8601 dates where it declares IfcDateTimeSelect (AuthoringValueType); a missing TaskId, TimeLag or ProcedureType (AuthoringRequired); and a USERDEFINED procedure, whose IFC2X3 WR4 needs a UserDefinedProcedureType the draft cannot carry (InvalidValue). Enumeration tokens are checked against the bound release, so an IFC4X3-only IfcTaskTypeEnum token is refused in IFC4.
  • ScheduleAuthoringError::MultipleSchemas, UnsupportedSchema, EntityNotInSchema, AuthoringNotInSchema, AuthoringValueType, AuthoringRequired, MissingReference and WrongReferenceType, appended after InvalidValue. The enum is #[non_exhaustive], so this is not breaking. SchemaVersion is re-exported, which they name. The crate now depends on ifc-schema for the bundled release tables.

Changed ​

  • The plain IfcRoot writers (create_task, create_sequence, create_work_control, assign_tasks_to_control, nest_tasks, create_work_calendar, create_event, create_procedure) take no model and so cannot see the declared release. Their signatures and output are unchanged: the layout IFC4 and IFC4X3 share, with OwnerHistory written $. They are now documented as IFC4/IFC4X3 only; in IFC2X3, where that record is invalid, use the *_with_owner_history variants (#202). create_recurrence_pattern moved to its own module; its path and behaviour are unchanged.

Fixed ​

  • create_lag_time writes IfcLagTime.LagValue as the member of IfcTimeOrRatioSelect it is (#201): a string as IFCDURATION('P5D') and a number as IFCRATIOMEASURE(0.5), an integer as that REAL. A bare value in a SELECT slot does not say which member it is. A value already typed as IFCDURATION or IFCRATIOMEASURE is accepted; any other is refused, as before. sequences reads both forms, as before.

ifc-schema ​

Added ​

  • Schema::subtypes and Schema::direct_subtypes: every entity inheriting from a name, the inverse of is_a. Checked against is_a for every ordered entity pair of all three bundled schemas (#32).

Changed ​

  • Requires openbim-step 0.5.1, which provides the downward walk.

ifc-spatial ​

Added ​

  • SpatialTree::anomalies() and SpatialAnomaly (#54). An element placed by two IfcRelContainedInSpatialStructures is reported as ContainedTwice, and a container aggregated by two parents as AggregatedTwice. Each names the element or child, the kept and rejected parent, and the rejected relationship. The first relationship applied still wins. Restating the same parent is not reported.

Changed ​

  • For invalid files only: the rejected container no longer lists a doubly contained element in SpatialNode::elements / elements_of. Before, the element appeared in both containers while container_of returned only the first, so the two views disagreed. Valid files are unaffected.

ifc-step ​

Changed ​

  • Requires openbim-step 0.5.1, matching ifc-schema 0.2.1. Both pin the parser exactly, so the pair must move together.

ifc-structural ​

Added ​

  • stage_boundary_condition_in(tx, schema, kind, draft) (#200, #201). It lays the record out by schema's attribute names (IFC2X3 still says LinearStiffness...) and writes each stiffness in the form its declared type requires there: bare in IFC2X3, whose stiffnesses are plain measures, and the typed parameter of the SELECT member in IFC4 and IFC4X3. A boolean where the release admits none (every IFC2X3 stiffness) is refused with InvalidDraftValue. It takes &Schema like the crate's other writers.

Fixed ​

  • stage_boundary_condition writes the IFC4/IFC4X3 form correctly (#200, #201):

    • an edge condition's translational stiffness is IFCMODULUSOFLINEARSUBGRADEREACTIONMEASURE(..), the member of IfcModulusOfTranslationalSubgradeReactionSelect, instead of IFCMODULUSOFTRANSLATIONALSUBGRADEREACTIONMEASURE, which no release declares;
    • warping is IFCWARPINGMOMENTMEASURE(..), the member of IfcWarpingStiffnessSelect, instead of IFCROTATIONALSTIFFNESSMEASURE;
    • a boolean stiffness is IFCBOOLEAN(.T.) instead of a bare .T., which does not say which SELECT member it is.

    It is now stage_boundary_condition_in with the bundled IFC4 table, so it is still not correct in IFC2X3; its docs point IFC2X3 callers to the new writer. The readers accept a bare and a typed value, as before.

ifc-systems ​

Fixed ​

  • IFC2X3 models are read against the IFC2X3 schema (#52). Before, every model was read with IFC4 ancestry, which gave wrong answers on IFC2X3:
    • systems() listed IfcZone as a system and rejected IfcElectricalCircuit as NotASystem. In IFC2X3 a zone is a plain group and an electrical circuit is a system; both are now read that way. A relationship assigning members to an IFC2X3 zone is still reported by systems() as NotASystem, since a zone is not a system there.
    • zones() checks WR1 against the declared release. IFC2X3 admits IfcZone and IfcSpace members only; it has no IfcSpatialZone.
    • ElementRole::of, ports(), and ConnectionGraph::build use the declared release's ancestry, so an IFC4-only record in an IFC2X3 file (such as IfcPipeSegment) has no role.

Added ​

  • schema_of(model) reports the release reads bind to, or why none could be bound (SchemaResolutionError: missing, multiple, or unsupported, including IFC4X3). SchemaVersion is re-exported.
  • long_name_of(model, zone) reads IfcZone.LongName and returns SchemaGap::NotInSchema when the release has no such attribute (IFC2X3), instead of a None that reads as "authored empty".

Unchanged ​

  • IFC4 models, and models with no FILE_SCHEMA (every in-memory model), read exactly as in 0.2.0. Every existing test passes unmodified.
  • The bulk readers keep their signatures and have no error channel. A header they cannot bind (none, several, or IFC4X3) reads against IFC4, as in 0.2.0. Call schema_of first to refuse such a model.
  • Authoring stays IFC4-only.

Verified ​

  • On three real IFC2X3 ventilation models (2 systems each), six other IFC2X3/IFC4 models, and an IFC4 MEP model with 16 systems, 11,666 ports, and 5,833 connections, the results are identical before and after. None of the local IFC2X3 files contains a zone, electrical circuit, or port, so the IFC2X3-specific paths are proven by the fixture tests only.

ifc-tabular ​

Added ​

  • Borrowed read views: TabularView reads IfcTable (rows and columns) and IfcRegularTimeSeries/IfcIrregularTimeSeries with their value records under a declared IFC4 or IFC4X3 schema, locating slots by name. WR1 (ragged row), WR2 (more than one heading), malformed slots, arity mismatches, empty lists and dangling or mistyped references are reported as TabularIssues instead of being dropped. IFC2x3 is refused with TabularReadError::UnsupportedSchema (#120).

ifc-template-catalog ​

Fixed ​

  • The built-in environmental advisories cite their decision record at its restored path, docs/adr/0017-versioned-psd-qto-catalog.md; the old 0010 path had been reassigned to an unrelated ADR.

ifc-xml ​

Fixed ​

  • A codec built with XmlCodec::with_schema now reads its own output back in slot order. Scalars are written as XML attributes and structured values as child elements, and the reader kept that document order, so IfcLocalPlacement($, #2) came back as (#2, $). Schema attribute names now resolve to slots through the same schema; found by the corpus round trip (#118).
  • Whitespace in element text is data: a padded string, enum or binary inside a list or typed wrapper no longer loses leading or trailing whitespace (#116).
  • A string such as " i7" no longer reads back as a reference. The writer now decides attribute versus element by running the reader's own inference, so the two cannot disagree (#116).
  • An attribute literal that overflows to infinity, such as 1e999, reads as text instead of an infinite real, matching the explicit kind="real" path and the writer, which both refuse non-finite reals (#116).
  • kind="logical" text other than true, false or unknown is an InvalidScalar error instead of silently becoming unknown (#116).
  • Tab, line feed and carriage return are written as character references, which conforming XML parsers do not normalize away, and a typed wrapper's type attribute is escaped (#116).

Added ​

  • XmlError::DuplicateSlot and XmlError::MissingSlot: two values for one attribute slot, or a positional a<i> name that skips slots without a schema declaring them, are refused with the entity path instead of shifting later attributes. With a schema, an omitted attribute reads as unset.
  • A seeded property test of the scalar contract and a differential STEP -> ifcXML -> Model -> STEP round trip over every committed fixture, in positional, schema-named and strict-profile configurations (#116, #118).

[0.2.0] - 2026-10-13 ​

ifc-alignment ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-approval ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-author ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-classification ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-constraint ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-control ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-cost ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-element-type ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-geometry ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-georef ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-material ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-model ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-occurrence ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-properties ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-resource ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-schedule ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-schema ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-spatial ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-step ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-structural ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-style ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-systems ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-tabular ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-template-catalog ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-validate ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

ifc-xml ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

openbim-ifc ​

First release under per-crate versioning. See the repository changelog for the family-wide history that produced this version.

openbim-ifc-cli ​

Added (#450) ​

  • convert reads and writes ifcZIP: an input archive is recognized by its ZIP signature on every command, and an output ending in .ifczip, or --to ifczip, is written as one Deflate entry named after the output file, STEP unless --inner ifcxml (with --layout) asks for ifcXML. An archive without exactly one model, or whose entry is in another format, is refused as parse; an encrypted entry or another compression method as unsupported; an entry past the decompression budget as budget-exceeded. info reports the format as STEP in ifcZIP (model.ifc).

Fixed ​

  • A native-layout ifcXML document whose attributes are named by position (a0, as the bindings' write_ifcxml writes it) reads, with the declared release's tables resolving both kinds of name; a strict read refused it.

Semver: minor (additive).

Changed (#463) ​

  • An entity named by GlobalId is found with the facade's by_guid instead of the command's own scan. A GlobalId several entities carry still exits 2 naming every holder (previously the first two), and a 22-character reference outside IFC's GlobalId alphabet or range is a usage error rather than a lookup that finds nothing.

Semver: patch.

[0.1.0] - 2026-10-09 ​

openbim-ifc-cli ​

Added (#329, the first part) ​

  • The openbim-ifc command, over the openbim-ifc facade only:
    • validate FILE...: each file against the schema its header declares; --format human|json|sarif, --max-findings, --deny-warnings, --include-unsupported. Exit 0 when every file passes, 1 on an error, an evaluation error, a truncated report or (with --deny-warnings) a warning, 2 when a file could not be validated.
    • convert IN OUT: STEP to ifcXML and back, the output format from the extension or --to; --layout native|xsd for ifcXML output; --input-layout auto|native|xsd; - writes standard output.
    • info FILE: header, declared schema and whether it is bundled, entity and per-type counts; --format human|json.
    • psets FILE ENTITY: the exactly resolved property and quantity sets of an object or type object, by #id or GlobalId, occurrence and inherited; --format table|json|csv.
    • tree FILE: the spatial structure with element counts or (--elements) the elements, and the anomalies the tree found; --format human|json.
    • lint FILE...: products no model viewer will draw (exit 1 on a finding); --format human|json|sarif.
  • Typed refusals with a stable kind on stderr (usage, io, parse, unsupported-schema, unsupported, missing-entity, wrong-entity-type, missing-reference, invalid-model, budget-exceeded, write) and exit code 2.
  • Prebuilt binaries on each openbim-ifc-cli-v* GitHub release for Linux (x86_64, aarch64; static musl), macOS (x86_64, arm64) and Windows (x86_64), with SHA256SUMS; install.sh and a Homebrew formula generated for the release; cargo binstall metadata.

Added (#376, #377, #378) ​

  • Debian packages openbim-ifc_<version>_amd64.deb and _arm64.deb on each release, packed from the same static binaries by scripts/package.py deb (the binary in /usr/bin, the README and the licence in /usr/share/doc/openbim-ifc/, no dependencies) and listed in SHA256SUMS; scripts/check-deb.sh inspects them and installs, runs and removes them with dpkg on Ubuntu 22.04 (x86_64 and aarch64) in CI and in every release build. No apt repository.
  • A Nix flake at the repository root: packages.<system>.openbim-ifc (and default) built from source with the pinned toolchain and Cargo.lock, and apps.<system>.default, so nix run github:openbimrs/ifc -- --version works on x86_64 and aarch64 Linux and macOS; checked in .github/workflows/nix.yml.
  • install.ps1 for Windows (irm .../install.ps1 | iex): picks the x64 or Arm64 archive, refuses a checksum mismatch, installs to %LOCALAPPDATA%\Programs\openbim-ifc\bin (or -Prefix) and offers to add it to the user PATH; honours OPENBIM_IFC_BASE_URL like install.sh. scripts/check-install.ps1 tests it on Windows in CI and in every release build, and the release attaches it.
  • A Windows Arm64 binary (aarch64-pc-windows-msvc) on each release.

Semver: a new crate, first release 0.1.0.

Released under the AGPL-3.0-or-later licence. ISO and CEN standards material is not redistributed.