ifc

package module
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 7 Imported by: 0

README

goifc

goifc

Go Reference CI CodeQL Go 1.25

codecov govulncheck

Read an IFC model from Go. No CGO, no IfcOpenShell, no OCCT, no Python sidecar. One go get, one static binary.

Documentation — guides, concepts and the compatibility policy.

API reference: ifc · step · model · geometry

Should you use it

Pick goifc when deployment cost dominates and bounding numbers are good enough. Pick IfcOpenShell when the geometry has to be exact — it is the better library, and it is also a C++ toolchain, a Python runtime, and a container several times the size of the service using it. goifc is the subset you need to parse the file, walk the semantics, and tessellate enough to get numbers.

The feature-by-feature comparison is on the docs site.

Install

go get github.com/blox-eng/goifc

Quickstart

package main

import (
	"bytes"
	"fmt"
	"os"

	ifc "github.com/blox-eng/goifc"
	"github.com/blox-eng/goifc/step"
)

func main() {
	src, err := os.ReadFile("model.ifc")
	if err != nil {
		panic(err)
	}

	f, err := step.ParseBytes(src)
	if err != nil {
		panic(err)
	}

	a, err := ifc.Assemble(f)
	if err != nil {
		panic(err)
	}

	for i := range a.Result.Elements {
		e := a.Result.Elements[i]
		if e.Qto.Volume == nil {
			continue // no volume for this element — see the next section
		}
		fmt.Printf("%s\t%.3f m³\t(%s)\n", e.Name, *e.Qto.Volume, e.QuantitySource)
	}

	var glb bytes.Buffer
	a.Scene.WriteGLB(&glb) // proxy geometry for a viewer
}

The package is named ifc, not goifc — alias the import as above.

Assemble gives you a flat list. ifc.BuildImport(f) is the other entry point: the same elements as a parents-first tree with spatial containers, per-type material layers and pre-baked floor plans — the call Blox actually ships. See getting started.

The numbers are labelled, and some of them are bounds

Every element reports where its quantities came from — "qto" for an authored IfcElementQuantity (net, from the modeller), "geometry" for one derived from the proxy mesh (gross — a wall over-reports by its windows and doors), and "none" where neither exists, never a fabricated 0.0.

That tag is the most important thing to understand before trusting a total: quantities and provenance.

The meshes are proxy geometry for visualization, not a B-rep substitute — do not clash-detect with them. The rest of the edges, stated plainly, are in limitations.

More

Compatibility

The API is unstable pre-1.0 — expect breaking changes on minor versions, and pin a version. Used in production by Blox, whose import pipeline is the only consumer this has been hardened against, so the well-trodden path is BuildImport on architectural IFC exports; off that path, expect to find edges.

Both of those have a fuller answer, including the serialization contracts that hold steady even when the Go API does not, in the compatibility policy.

Contributing

Issues and PRs welcome — the open issues are the roadmap. See CONTRIBUTING.md. Commits follow Conventional Commits; CI enforces it.

License

MIT — see LICENSE.

Documentation

Overview

Package ifc is the top-level entry to the goifc Go-native IFC engine. It wires the per-stage packages — step (STEP/EXPRESS parse) -> model (semantic extraction) -> geometry (proxy tessellation + derived quantities) — into a single Assemble call that turns a parsed STEP file into quantity-back-filled semantic elements plus their proxy geometry:

f, _ := step.ParseBytes(src)
a, _ := ifc.Assemble(f)
for i := range a.Result.Elements {
	e := a.Result.Elements[i] // e.Qto, e.QuantitySource ("qto"|"geometry"|"none")
}
a.Scene.WriteGLB(w) // proxy geometry for a viewer

KNOWN LIMITATION: the geometry-derived Volume tier is GROSS. For walls with openings the extrude path reports the SOLID (un-subtracted) volume, so those elements over-report versus ifcopenshell's NET figure. The quantity_source="geometry" tag already flags these as bounding estimates, so no consumer mistakes one for an authored net Qto — netting openings out of the extrude volume is deliberately out of scope here.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Assembled

type Assembled struct {
	Result *model.Result
	Scene  *geometry.Scene
	// contains filtered or unexported fields
}

Assembled is the end-to-end output of Assemble: the quantity-back-filled semantic Result — the []model.Element that downstream consumers golden-diff and import — paired with the proxy-geometry Scene it was derived from, for WriteGLB and mesh Stats. Elements in the two share identity via GlobalID.

func Assemble

func Assemble(f *step.File) (*Assembled, error)

Assemble runs the full ifc pipeline over a parsed STEP file, in order:

model.Extract          semantic elements ([]model.Element)
geometry.Build         proxy geometry per element
Scene.DerivedQuantities tier-2 GROSS quantities from the meshes
Result.ApplyDerivedQuantities back-fills them onto un-authored elements

It is the single production entry point for the engine: it chains the four stages so callers never have to reinvent the chain.

Quantity tiering follows model.ApplyDerivedQuantities: authored Qto (tier-1, NET) always wins; where absent, the GROSS geometry-derived quantities back-fill and the element is tagged quantity_source="geometry"; an element with neither stays "none" — never a fabricated 0.0.

Example

ExampleAssemble shows the end-to-end entry: raw IFC bytes -> parse -> Assemble -> per-element quantities + a GLB export, all from one orchestration call.

f, err := step.ParseBytes([]byte(boxIFC))
if err != nil {
	fmt.Println("parse:", err)
	return
}
a, err := ifc.Assemble(f)
if err != nil {
	fmt.Println("assemble:", err)
	return
}

fmt.Println("elements:", len(a.Result.Elements))
for i := range a.Result.Elements {
	e := a.Result.Elements[i]
	h := 0.0
	if e.Qto.Height != nil {
		h = *e.Qto.Height
	}
	fmt.Printf("%s source=%s height=%.2fm\n", e.Name, e.QuantitySource, h)
}

var glb bytes.Buffer
fmt.Println("glb written:", a.Scene.WriteGLB(&glb) == nil && glb.Len() > 0)
Output:
elements: 1
Box source=geometry height=1.00m
glb written: true

type ImportModel

type ImportModel struct {
	Nodes       []ImportNode
	Scene       *geometry.Scene
	StoreyPlans []StoreyPlan
	// TypeLayers is the build-up per distinct IfcTypeObject, keyed by the type's
	// GlobalId — the same key ImportNode.TypeGlobalID carries. Keyed by TYPE, not
	// by occurrence: a real model has ~70 types and ~1,400 typed occurrences, and
	// this whole struct is typically serialized into a workflow payload with a
	// low-megabyte size cap.
	//
	// A type that resolved but carries no ordered build-up is PRESENT with an
	// empty Layers slice — that is a positive claim ("this type has no layers"),
	// distinct from absence ("no such type in this model"), and consumers
	// reconciling previously-imported layers need the difference: the first must
	// retire every stale row, the second must touch nothing.
	TypeLayers map[string]TypeLayerSet
}

ImportModel is the assembled import contract: the parents-first node tree plus the proxy-geometry Scene the physical nodes' meshes are baked from (Scene.WriteGLB).

func BuildImport

func BuildImport(f *step.File) (*ImportModel, error)

BuildImport turns a parsed STEP file into the import contract, assembling the model first. It is BuildImportFrom over a fresh Assemble — use that directly when you also need the Result or Scene the import was derived from, so the model is tessellated once rather than twice.

func BuildImportFrom added in v0.7.0

func BuildImportFrom(f *step.File, a *Assembled) (*ImportModel, error)

BuildImportFrom turns a parsed STEP file plus its assembly into the import contract: spatial containers (SpatialNodes) + physical elements in ONE parents-first ordered tree.

Assemble is the expensive stage, and its Result and Scene are worth more than the import contract alone: an elevation, a net-area reconciliation or a GLB all read them. Taking the assembly as an argument is what lets one caller have both without paying for the tessellation twice.

f must be the same *step.File the assembly was built from. Below this point everything joins physical elements to spatial containers and geometry by bare ExpressID (forwardParentMap, f.ByID, Scene.NetAreas), and ExpressIDs are small sequential integers that restart per file — a mismatched pair would very plausibly collide rather than miss, handing an element another model's parent, material or opening data with no error at all. An Assembled stamped by Assemble is checked by pointer identity against f, on purpose: a re-parse of the same bytes produces an equal-but-distinct *step.File, and that is exactly the mismatch this guards against. An Assembled built some other way carries no stamp and is let through unchecked — that caller assembled Result and Scene itself and already owns the pairing.

parent map  = IfcRelAggregates ∪ IfcRelContainedInSpatialStructure, FORWARD
              (iterate each rel's Related* → RelatingObject/Structure). NEVER
              per-node model.Container — Container(storey) self-parents (a storey
              is the RelatingStructure of its own containment rels).
order       = topo BFS from roots; a parent always precedes its child, so
              a consumer's parentID[*ParentIndex] lookup never misses.
geometry    = joined to physical nodes by GlobalID (the emitted order is NOT
              index-aligned with Scene.Elements once spatial nodes are interleaved).

type ImportNode

type ImportNode struct {
	GlobalID       string
	ExpressID      int
	IFCClass       string
	Name           string
	ParentIndex    *int // index into the emitted slice; nil = import root
	Qto            model.Quantities
	QuantitySource string
	OriginMin      [3]float64 // world-AABB min (transform.origin); zero if no geometry
	BBoxMin        [3]float64
	BBoxMax        [3]float64
	Material       string   // model.Element.Material; "" when none
	IsExternal     *bool    // *Common.IsExternal tri-state; nil when unknown
	NetArea        *float64 // trusted net area (m²) from Scene.NetAreas; nil when absent/untrusted
	// OpeningPerimeter is the boundary length (m) of the opening union whose
	// area NetArea already nets out — the length facade trades bill reveals
	// along. Present exactly when NetArea is: both come from one trusted
	// reconciliation, so a consumer can never read a confident perimeter beside
	// an absent net.
	OpeningPerimeter *float64
	// OpeningDeduction is the area (m²) of that same opening union, and
	// ProjectedGross is the host's own silhouette on the same plane — the two
	// halves NetArea is the difference of. Both are measured on the host's
	// winning projection axis, which is what makes them a matched pair.
	//
	// They are published because NetArea alone cannot be aggregated: hosts with
	// no IfcRelVoidsElement are ABSENT from the reconciliation entirely, so
	// summing NetArea over a facade silently drops every solid wall. Netting a
	// total therefore means subtracting the DEDUCTION from whatever gross the
	// caller is totalling, not summing the nets.
	//
	// That matters most when the gross being netted is measured differently.
	// [geometry.Facing].FaceArea is the true on-face area and does not
	// foreshorten, while these two are a projection and do. The projection
	// foreshortens gross and deduction by the SAME factor, so that factor is
	// recoverable as FaceArea/ProjectedGross and a caller netting an on-face
	// gross computes:
	//
	//	net = FaceArea - OpeningDeduction*(FaceArea/ProjectedGross)
	//
	// NOT FaceArea - OpeningDeduction, which under-deducts by that factor.
	// Without ProjectedGross the bias is not merely uncorrected, it is
	// invisible.
	//
	// Present exactly when NetArea is, for the reason OpeningPerimeter is: all
	// four come from one trusted reconciliation. A zero deduction here means a
	// host whose openings measured zero, never an untrusted one.
	OpeningDeduction *float64
	ProjectedGross   *float64
	// HasOpenings reports whether this element carries IfcRelVoidsElement
	// openings at all, which is the fact the three nil-able fields above CANNOT
	// express. They are absent for two opposite reasons — the host has no
	// openings, so its net equals its gross; or its reconciliation was refused,
	// so its net is unknown — and a consumer netting a total must tell those
	// apart. Reading absence as "no openings" reports a fully-glazed wall as
	// solid; reading it as "unknown" drops every solid wall from the total.
	//
	// Unlike the others this is always meaningful, so it is a plain bool: there
	// is no third state to encode.
	HasOpenings bool

	// TypeGlobalID / TypeName / TypeClass identify the element's IfcTypeObject.
	// Empty when the element carries no IfcRelDefinesByType — most elements do.
	TypeGlobalID string
	TypeName     string
	TypeClass    string
}

ImportNode is one node of the import contract: spatial containers + physical elements in a single parents-first tree. Spatial nodes have no geometry, so their AABB stays zero.

type StoreyEntity

type StoreyEntity struct {
	GlobalID string
	IFCClass string
	Loops    []geometry.Loop
}

StoreyEntity is one element's plan geometry on a storey: its footprint loops (world XY meters, Y-up), tagged by IFC class, keyed by GlobalID. Consumers typically resolve GlobalID to their own domain object id when rendering the storey's floor plan.

type StoreyPlan

type StoreyPlan struct {
	StoreyGlobalID string
	Elevation      float64 // meters, for UI ordering (StoreyElevations; 0 if absent)
	Entities       []StoreyEntity
}

StoreyPlan is one IfcBuildingStorey's 2D floor plan: the entities a horizontal section at cutZ = floorZ + 1.2 m draws, chosen by geometric membership.

type TypeLayerSet

type TypeLayerSet struct {
	Layers    []model.MaterialLayer
	Direction string
	Sense     string
}

TypeLayerSet is one IfcTypeObject's assembly build-up, in declared EXPRESS LIST order. Direction/Sense are the raw IFC labels from IfcMaterialLayerSetUsage (AXIS1/AXIS2/AXIS3, POSITIVE/NEGATIVE); mapping them onto a product vocabulary belongs to the consumer.

Directories

Path Synopsis
Package geometry builds proxy geometry (a view of the semantic model, no CAD kernel) from a parsed IFC step.File plus the model package's model.Result, and emits a single Y-up GLB whose node names are element GlobalIds.
Package geometry builds proxy geometry (a view of the semantic model, no CAD kernel) from a parsed IFC step.File plus the model package's model.Result, and emits a single Y-up GLB whose node names are element GlobalIds.
Package model walks a parsed STEP/IFC entity graph (see the step package) into a canonical semantic []Element.
Package model walks a parsed STEP/IFC entity graph (see the step package) into a canonical semantic []Element.
Package step is a schema-agnostic STEP/SPF (ISO 10303-21) tokenizer and entity graph for IFC files, ported from ifcopenshell's parser and entity_instance model into idiomatic Go.
Package step is a schema-agnostic STEP/SPF (ISO 10303-21) tokenizer and entity graph for IFC files, ported from ifcopenshell's parser and entity_instance model into idiomatic Go.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL