Skip to content

2D approval plans (Baugenehmigung) ​

Scenario. A user imports an IFC model, derives 2D floor plans from it, annotates those plans with the construction symbols required for a German building-permit submission (Baugenehmigung), and keeps the result IFC-native — symbols carried as IfcAnnotation, symbol definitions referenced through IfcLibraryReference, and sign-off recorded with IfcApproval.

This page maps that scenario onto the current code. It is written to be read before the first line of application code is written.

Verdict up front ​

openbim-ifc gives you a correct, lossless IFC substrate, real 2D curve geometry, schema-checked authoring, spatial traversal, and bounded typed presentation, annotation, library and approval semantics. It does not give you plan sectioning, annotation placement, or rendering.

That distinction matters for this use case. You can construct any entity the schema declares — including IfcAnnotation, IfcCurveStyle, IfcLibraryReference and IfcApproval — by naming its attributes, and the builder will refuse a wrong arity or type. ifc-style adds strict borrowed views and selected transaction-staged writers for annotations and presentation data; ifc-classification does the same for library references and ifc-approval for approvals. The repository lacks the sectioning geometry needed to derive a plan from a 3D body.

Scenario stepServed by this crate?
Read .ifc / .ifcxml losslesslyYes
Preserve every entity you do not interpretYes
Find products and their representationsPartly
Walk the spatial tree (storey → elements)Yes — via ifc-spatial
Read 2D curve attributes (IfcPolyline, IfcCircle, …)Yes
Lower 2D curves into the neutral geometry graphYes — as representation items or through the items that own them
Select the 2D (Plan/Annotation) representationYes — via select_plan_representation
Cut a section / derive a plan from 3D bodiesNo
Author IfcAnnotation symbolsYes — bounded helpers in ifc-style; generic fallback in ifc-author
Write style entities (IfcCurveStyle, …)Yes — selected bounded helpers in ifc-style; generic fallback in ifc-author
Interpret style entities as a typed viewYes — bounded presentation domain in ifc-style
Write IfcLibraryReferenceYes — via ifc-classification or generic ifc-author
Write IfcApprovalYes — via ifc-approval or generic ifc-author
Interpret library / approval entities as typed viewsYes — ifc-classification and ifc-approval
Write the result back outYes

Try it: a downloadable round-trip file ​

annotation-plan.ifc (2.8 KB, IFC4) is one fire-wall symbol drawn the way this page recommends:

EntityIn the file
IfcAnnotationBrandwand, contained in storey EG
IfcShapeRepresentationAnnotation2D, in a PLAN_VIEW Annotation sub-context at 1:100
IfcPolylinethe wall line, 6000 mm
IfcTextLiteralWithExtentBrandwand F90, a 2000 × 300 mm box, bottom-left
IfcCurveStyle, IfcTextStyle, IfcStyledItemred, 50 mm line; Arial 250 mm text
IfcPresentationLayerAssignmentlayer A-ANNO-FIRE
IfcLibraryReference, IfcRelAssociatesLibrarysymbol BW-F90 in library Brandschutz-Symbole

Read it, change the label through a schema-checked edit, and write it back:

rust
use ifc::style::StyleView; // features = ["step", "style", "author"]
use ifc::{Codec, EntityEditor, StepCodec, Transaction};

let mut model = StepCodec.read_bytes(&bytes)?;
let schema = ifc::schema::ifc4();

let text = model.ids_of_type("IFCTEXTLITERALWITHEXTENT")[0];
let view = StyleView::new(&model, schema);
assert_eq!(view.text_literal(text)?.literal()?, "Brandwand F90");

let mut tx = Transaction::new(&model);
EntityEditor::new(schema, &model, text)?
    .text("Literal", "Brandwand F90 (geprüft)")
    .stage(&mut tx)?;
tx.commit(&mut model)
    .map_err(|conflicts| format!("{conflicts:?}"))?;

let out = StepCodec.write_bytes(&model)?;

Written back, every entity is unchanged apart from that one label, and the annotation, text, styles, layer and library reference all read back through their typed views. crates/openbim-ifc/tests/docs_examples.rs runs exactly this against the published file.

What the file does not show: there is no IfcApproval (see approvals and constraints for sign-off), and nothing here renders. The styles are read back as data; drawing them is your application's job.

The file is synthetic and generated by tools/gen_annotation_fixture.py with IfcOpenShell, which validates it (including EXPRESS rules) before writing. It contains no third-party or normative material, and is licensed with the rest of the repository under AGPL-3.0-or-later.

What works today ​

Lossless import and export ​

This is the strongest guarantee the project makes, and it is the one that makes the rest of the gaps survivable. ifc-model stores entities structurally — a type name plus positional attribute values — so entities the application never interprets are still preserved byte-faithfully through a read/write cycle.

Practically: your application can read a model containing IfcAnnotation, IfcApproval, and IfcLibraryReference entities authored by another tool, modify only what it understands, and write the file back without destroying them. That property holds today, with no domain crate compiled.

rust
use ifc::{Codec, StepCodec};

let model = StepCodec.read_bytes(source)?;

// Entities of any type survive, interpreted or not.
let annotations = model.ids_of_type("IFCANNOTATION");
println!("{} annotations passed through untouched", annotations.len());

let out = StepCodec.write_bytes(&model)?;

Note ids_of_type takes the upper-case STEP type name.

2D curve geometry ​

ifc-geometry provides borrowed typed views for the curve families used by 2D plans and lowers them into exact neutral values: lines, polylines, indexed poly-curves, circles, ellipses, trimmed and composite curves, offset curves, and explicit-knot B-splines with optional rational weights. Parameter units, segment sense, transition codes, trims and closure are preserved, not approximated.

A curve lowers both as a representation item of its own (the lines of a FootPrint or Annotation representation) and through the item that owns it: a sweep directrix, a surface boundary, a B-rep edge, or an IfcGeometricCurveSet member. The exact status of every family is in the generated lowering table.

This is neutral geometry, not a complete permit-drawing pipeline. The crate does not section 3D bodies, interpret curve styles, place annotations, or render the result; an application must still provide those layers.

Unit resolution (units.rs) handles SI and conversion-based units, which matters because plan dimensions in millimetres versus metres is a classic silent-corruption bug in permit drawings.

Reading a product's representations ​

ifc-geometry::input::representation gives you ProductShape and Representation views, so you can enumerate a product's representations and read each one's RepresentationIdentifier (Body, Axis, FootPrint, …).

The four things you must build ​

1. A 2D representation selector — provided ​

select_shape_representation is written for a 3D viewer and deliberately refuses Axis/FootPrint. select_plan_representation is its inverse and now ships:

rust
use ifc::select_plan_representation; // feature = "geometry-select"

let drawable = select_plan_representation(&model, wall)?;

It prefers a drawable identifier authored into an explicit PLAN_VIEW sub-context, then falls back to Plan, Annotation, FootPrint, Axis in that order. The two conditions are intersected: exporters write Box/BoundingBox representations into plan contexts, and a bounding box is not a drawing whatever context it sits in.

Selection is the light half of the crate. geometry-select gives you contexts, plan/body choice, placements and units without compiling a solid modeller; geometry adds lowering into the neutral DAG:

bash
cargo add openbim-ifc --features geometry-select

That build links no axiolid-* geometry crate at all; crates/ifc-geometry/tests/kernel_free_build.rs keeps it that way.

None means the product carries only solid or bounding-box geometry. That is a real answer: turning a solid into a plan needs sectioning, which is still §3 below.

Where a product sits in the world ​

Selecting a representation gives geometry in the product's own local space. To draw it you need the world transform, which means resolving the IfcLocalPlacement chain up through storey, building and site:

rust
use ifc::product_world_transform; // feature = "geometry-select"

let world = product_world_transform(&model, &units, wall)?;

Do not hand-roll this. The two mistakes are invisible until late: composing the chain innermost-first mirrors the model about its ancestors, and applying the unit scale per link instead of once raises the factor to the power of the chain depth, so a millimetre file three levels deep lands a thousand times too far out. Cyclic chains in malformed files are reported rather than hung on.

For a whole-model walk use the batch form, which shares one placement cache -- every product in a storey shares that storey's entire ancestor chain:

rust
use ifc::products_world_transforms;

for (id, world) in products_world_transforms(&model, &units, ids) {
    // Errors are per product: one broken chain does not hide the rest.
    match world {
        Ok(world) => println!("{id} sits at {:?}", world.origin),
        Err(error) => eprintln!("{id}: {error}"),
    }
}

Plan contexts are readable too, including the * inheritance real exporters write:

rust
use ifc::plan_contexts; // feature = "geometry-select"

for context in plan_contexts(&model) {
    let scale = context.target_scale(); // Some(0.01) for 1:100
    let precision = context.precision(&model); // inherited from the parent context
    println!("plan view at {scale:?}, precision {precision:?}");
}

2. Spatial tree traversal — provided ​

ifc-spatial builds the tree. Grouping elements by storey, the query a floor plan is organised around, is:

rust
use ifc::{SpatialKind, SpatialTree}; // feature = "spatial"

let tree = SpatialTree::build(&model);
for level in tree.of_kind(SpatialKind::Storey) {
    let on_this_level = tree.elements_of(level.id);
    println!("{} elements on {}", on_this_level.len(), level.id);
}

container_of(element) answers the inverse — which storey a given wall is on — and elements_recursive descends through spaces. Anomalies a permit drawing would care about are reported rather than hidden: orphans() lists containers nothing aggregates, dangling() lists relationships naming absent entities.

One caution specific to this use case: a real corpus file was found that declares its whole hierarchy with IfcRelAggregates and contains no containment relationship at all. elements_of is correctly empty there. If your input comes from such an exporter, elements are associated by other means and you must handle that case explicitly rather than assuming an empty storey is a bug in the library.

3. Plan derivation from 3D geometry ​

If the source model has no FootPrint/Annotation representation — common — you must derive plan geometry by sectioning the 3D bodies at a cut height.

Nothing in this repository does that. ifc-geometry lowers bodies into the neutral Axiolid DAG and stops; sectioning is a kernel operation. Two further constraints:

  • Representation-item lowering remains partial overall, but IfcTriangulatedFaceSet and IfcPolygonalFaceSet — heavily used by IFC4 exporters — both lower today. Consult the generated dispatch table for the exact supported families.
  • A plane/solid section operation is an Axiolid capability question, not an IFC one. Check Axiolid's own capability page before assuming it exists.

Reduce scope where you can

If your product can require that incoming models carry authored 2D representations, steps 1 and 3 shrink dramatically. Many architectural exports do include FootPrint. Consider making full 3D sectioning a later milestone rather than a launch requirement.

4. Authoring: annotations, styles, libraries, approvals ​

This gap is closed for writing. ifc-style provides bounded transactional helpers for the named annotation and core style graphs. For entities outside that surface, ifc-author constructs any schema-declared entity by name:

rust
use ifc::EntityBuilder; // feature = "author"

let id = EntityBuilder::new(&schema, "IfcAnnotation")
    .text("GlobalId", "3vB2YO$MX4xv5uCqZZG05x")
    .text("Name", "Brandwand")
    .insert(&mut model)?;

Seven slots are produced because IFC4 declares seven, with GlobalId first because it is inherited from IfcRoot. A typo in the entity or attribute name, a duplicate set, a missing required attribute, a wrong value type, or a malformed GlobalId are all refused before anything reaches the model.

That covers every entity in this list — they are all declared by the schema:

What you must writeCan you author it?Is there a typed view to read it back?
IfcAnnotationYesYes (ifc-style)
IfcTextLiteralWithExtentYesYes (ifc-style)
IfcAnnotationFillAreaYesYes (ifc-style)
IfcCurveStyle, IfcFillAreaStyle, IfcStyledItemYesYes (ifc-style)
IfcPresentationLayerAssignmentYesYes (ifc-style)
IfcShapeRepresentationYesNo
IfcGeometricRepresentationSubContextYesYes (ifc-geometry)
IfcLibraryReference, IfcLibraryInformationYesYes (ifc-classification)
IfcRelAssociatesLibraryYesYes (ifc-classification)
IfcApproval, IfcRelAssociatesApprovalYesYes (ifc-approval)

Rows with a typed view expose domain meaning and strict reference/value errors. The remaining absent rows still round-trip structurally through ifc-model, but your application owns their interpretation.

What to still verify yourself

ifc-author checks arity and declared types. It does not check WHERE rules, inverse attributes, or whether your IfcRelAssociatesApproval actually points at a sensible object. Round-trip your output through StepCodec and open it in a reference tool before trusting it as a permit document.

Before you export: will a viewer actually draw it? ​

A file can be schema-valid, pass IfcOpenShell.validate with zero errors, and still open completely blank. Validation asks whether the file is legal IFC. Whether the geometry is reachable is a different question, and nothing in the schema answers it.

rust
use ifc::{unreachable_products, Codec, StepCodec}; // features = ["spatial", "geometry-select"]

let model = StepCodec.read_bytes(&bytes)?;
for (id, why) in unreachable_products(&model) {
    eprintln!("#{id}: {}", why.message());
}

Enable with features = ["step", "spatial", "geometry-select"].

Three causes are reported:

CauseWhy it is invisible
NotContainedInSpatialStructureViewers reach geometry by walking the spatial tree. A product outside it is never visited, however well-formed.
NoRepresentationInModelContextA body authored only into PlanView is skipped by a 3D viewer, which renders Model.
RepresentationWithoutContextGeometry a viewer cannot schedule, because the context reference does not resolve.

The second one is the trap specific to this use case. Annotations belong in a plan sub-context — that is correct authoring — but if the product's body also lives only there, the model viewer you check your work in shows nothing, and it is easy to conclude the export failed entirely.

What it will not tell you

Being outside the spatial tree is normal for openings (IfcRelVoidsElement), assembly parts (IfcRelAggregates), spatial containers themselves, and products with no representation. None of these are reported: on AC20-FZK-Haus.ifc, 20 of 127 products sit outside the containment tree and the lint finds nothing, because all 20 are legitimate. A lint that cries wolf is one you switch off.

  1. Import + export round-trip. Prove a file survives your pipeline unchanged before adding behaviour. Use openbim-ifc with features = ["step", "ifcxml", "author"].
  2. Thin authoring layer. No longer yours to build: enable features = ["step", "author"] and use EntityBuilder.
  3. Spatial grouping. Provided: SpatialTree from ifc-spatial.
  4. 2D representation selection. Provided: select_plan_representation.
  5. Annotation authoring. IfcAnnotation + IfcTextLiteralWithExtent + IfcCurveStyle, assigned into an annotation sub-context.
  6. Library references. IfcLibraryReference per symbol, associated via IfcRelAssociatesLibrary. This is what makes your symbol set portable rather than proprietary.
  7. Approvals. IfcApproval + IfcRelAssociatesApproval for sign-off state, through ifc-approval (feature approval).
  8. (Later) Plan derivation from 3D. Tessellated face-set lowering is available; a sectioning path is still required.

What would change this verdict ​

The repository owns the lossless substrate, plan-representation selection, bounded annotation/presentation semantics, and the library-reference and approval domains. The remaining gap for this scenario is 3D-to-plan sectioning, which is a kernel capability; placement, composition, and rendering remain application/provider responsibilities.

If you are building this application, those remaining roadmap items are the ones worth watching — or contributing to. See contributing.

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