Documentation
¶
Overview ¶
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. It parses a .ifc file in-process into a navigable graph of entity instances with forward AND inverse references, using no CAD kernel, no Python, and no EXPRESS schema.
Scope: pure SPF, not the schema ¶
A STEP file is purely positional: a record #5=IFCWALL('guid',#6,'name',...) stores attributes by position, never by name. This package exposes exactly what is recoverable from the raw stream; naming and type-hierarchy features are a separate schema layer built on top of it.
In scope (pure SPF, no schema) Out of scope (needs the EXPRESS schema) ---------------------------- --------------------------------------- attribute by INDEX (inst.Get(i)) attribute by NAME (inst.GlobalId) type keyword (inst.Type/IsA) is_a(supertype), subtype expansion forward refs (Value.Ref) named inverse attrs (.IsDecomposedBy) inverse graph (File.Inverse) derived-attribute formulas traverse, by-id, by-type (exact) by_guid, create_entity by name
IsA and ByType are exact-type only. Inverse exposes the raw referrer graph; projecting it into named IFC inverse attributes is the schema layer's job.
Usage ¶
f, err := step.ParseFile("model.ifc")
if err != nil { return err }
for _, wall := range f.ByType("IfcWall") {
if placement, ok := wall.Ref(5); ok {
_ = placement // #id resolved to *Instance
}
}
// who references this unit?
for _, referrer := range f.Inverse(f.ByType("IfcSiUnit")[0]) { _ = referrer }
Parsing is eager and in-memory: the whole file is tokenized into instances in two passes (record load, then reference resolution + inverse indexing). Dangling references are non-fatal and surface via File.Warnings.
Index ¶
- Constants
- type File
- func (f *File) All() iter.Seq[*Instance]
- func (f *File) ByID(id int) (*Instance, bool)
- func (f *File) ByType(keyword string) []*Instance
- func (f *File) Inverse(inst *Instance) []*Instance
- func (f *File) InverseIndices(inst *Instance) []InverseRef
- func (f *File) Len() int
- func (f *File) SchemaID() string
- func (f *File) TotalInverses(inst *Instance) int
- func (f *File) Traverse(inst *Instance, maxLevels int, order TraverseOrder) []*Instance
- func (f *File) Warnings() []string
- type Header
- type Instance
- func (i *Instance) Args() []Value
- func (i *Instance) File() *File
- func (i *Instance) Get(idx int) (Value, bool)
- func (i *Instance) ID() int
- func (i *Instance) IsA(keyword string) bool
- func (i *Instance) Len() int
- func (i *Instance) Ref(idx int) (*Instance, bool)
- func (i *Instance) Type() string
- func (i *Instance) Walk(fn func(Value))
- type InverseRef
- type Kind
- type ParseError
- type Scanner
- type Token
- type TokenKind
- type TraverseOrder
- type Value
Examples ¶
Constants ¶
const Unbounded = -1
Unbounded is the maxLevels value for an unbounded File.Traverse.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type File ¶
type File struct {
Head Header
// contains filtered or unexported fields
}
File is a parsed STEP model: the header plus a navigable entity graph. Lookups (ByID/ByType), the forward reference graph (resolved on Value.Ref), and the inverse index are all pure-SPF — no EXPRESS schema required.
func ParseBytes ¶
ParseBytes parses an in-memory STEP/SPF (ISO 10303-21) document into a navigable entity graph. It runs two passes: pass 1 tokenizes the HEADER and every DATA record into instances (references captured but unresolved) and builds the id and type indexes; pass 2 resolves references to instance pointers and builds the inverse index. Dangling references are non-fatal warnings.
Example ¶
package main
import (
"fmt"
"github.com/blox-eng/goifc/step"
)
// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`
func main() {
f, err := step.ParseBytes([]byte(sampleIFC))
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("schema:", f.SchemaID())
fmt.Println("instances:", f.Len())
}
Output: schema: IFC4 instances: 7
func (*File) All ¶
All returns an iterator over every instance in source (insertion) order. It allocates nothing and supports early termination:
for inst := range f.All() {
...
}
Example ¶
package main
import (
"fmt"
"github.com/blox-eng/goifc/step"
)
// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`
func main() {
f, _ := step.ParseBytes([]byte(sampleIFC))
count := 0
for range f.All() { // no allocation; supports break
count++
}
fmt.Println("total:", count)
}
Output: total: 7
func (*File) ByType ¶
ByType returns all instances whose exact type keyword matches (case-insensitive). Subtype expansion (all IfcElement subtypes) requires the schema and is out of scope here.
Example ¶
package main
import (
"fmt"
"github.com/blox-eng/goifc/step"
)
// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`
func main() {
f, _ := step.ParseBytes([]byte(sampleIFC))
for _, wall := range f.ByType("IfcWall") { // exact type, case-insensitive
name, _ := wall.Get(2) // positional attribute (schema-agnostic)
placement, _ := wall.Ref(5)
fmt.Printf("#%d %s name=%q placement=#%d\n",
wall.ID(), wall.Type(), name.Str, placement.ID())
}
}
Output: #10 IFCWALL name="Wall A" placement=#11
func (*File) Inverse ¶
Inverse returns the distinct instances that reference inst via any forward attribute (the raw referrer set). Order follows first-seen referrer.
Example ¶
package main
import (
"fmt"
"github.com/blox-eng/goifc/step"
)
// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`
func main() {
f, _ := step.ParseBytes([]byte(sampleIFC))
unit := f.ByType("IfcSiUnit")[0] // #3
for _, referrer := range f.Inverse(unit) {
fmt.Printf("#%d is referenced by #%d (%s)\n",
unit.ID(), referrer.ID(), referrer.Type())
}
}
Output: #3 is referenced by #2 (IFCUNITASSIGNMENT)
func (*File) InverseIndices ¶
func (f *File) InverseIndices(inst *Instance) []InverseRef
InverseIndices returns every (referrer, attribute-index) pair pointing at inst, including multiple entries for one referrer that references inst more than once. This is what a schema layer filters to build named inverse attributes.
func (*File) TotalInverses ¶
TotalInverses returns the count of distinct instances referencing inst.
func (*File) Traverse ¶
func (f *File) Traverse(inst *Instance, maxLevels int, order TraverseOrder) []*Instance
Traverse returns the forward transitive closure of inst (inst included), following resolved references through attributes, nested lists, and typed values. maxLevels is the depth limit: Unbounded (-1) for no limit, 0 for just inst. order selects DepthFirst or BreadthFirst. Each instance appears once.
Bounded traversal is depth-correct regardless of order: a node reachable within maxLevels is always included even if a longer path to it is explored first (bestDepth tracks the shortest known depth and allows a node to be re-expanded when a shorter path reaches it).
Example ¶
package main
import (
"fmt"
"sort"
"github.com/blox-eng/goifc/step"
)
// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`
func main() {
f, _ := step.ParseBytes([]byte(sampleIFC))
wall, _ := f.ByID(10)
var ids []int
for _, inst := range f.Traverse(wall, step.Unbounded, step.DepthFirst) {
ids = append(ids, inst.ID())
}
sort.Ints(ids)
fmt.Println("closure of #10:", ids)
}
Output: closure of #10: [10 11 12 13]
type Header ¶
type Header struct {
Description []string // FILE_DESCRIPTION descriptions
ImplementationLevel string // FILE_DESCRIPTION implementation_level
Name []string // FILE_NAME fields (raw, in declaration order)
Schema []string // FILE_SCHEMA identifiers, e.g. ["IFC2X3"]
}
Header holds the ISO-10303-21 HEADER section fields.
type Instance ¶
type Instance struct {
// contains filtered or unexported fields
}
Instance is a parsed STEP entity instance: a schema-agnostic entity_instance (ported from ifcopenshell). Attribute access is positional — access by NAME is a schema concern layered on by a later component. The type keyword and argument list come straight from the SPF record.
func (*Instance) IsA ¶
IsA reports whether this instance's exact type keyword equals keyword (case-insensitive). For a complex instance it matches any of its part types. This is exact-only: supertype checks (IfcWall IS-A IfcElement) require the EXPRESS schema and are out of scope here.
func (*Instance) Ref ¶
Ref returns the resolved instance referenced by attribute idx, if that attribute is a resolved reference (#id). ok is false when idx is out of range, not a ref, or a dangling ref whose target was missing.
type InverseRef ¶
InverseRef records one referrer of an instance: the referring instance and the top-level attribute index on it that holds the reference. This is the raw referrer graph; projecting it into named IFC inverse attributes is a schema concern for a later component.
type Kind ¶
type Kind uint8
Kind tags the variant of a parsed STEP attribute value. It mirrors the runtime categories ifcopenshell distinguishes from the SPF token alone (no schema): the declared EXPRESS type is a separate, schema-driven concern layered on later.
const ( KindNull Kind = iota // $ (unset / omitted optional) KindDerived // * (value derived in a supertype) KindInt // integer literal -> I KindFloat // real literal -> F KindString // '...' (decoded) -> Str KindEnum // .LABEL. -> Str (label, no dots) KindBool // .T./.F. -> B (EXPRESS BOOLEAN) KindLogical // .U. -> (no payload) EXPRESS LOGICAL "unknown", distinct from false KindBinary // "0..." binary -> Str (raw hex/bit text) KindRef // #id -> RefID, Ref (nil until resolved) KindList // (...) aggregate -> List KindTyped // KEYWORD(inner) -> Str (keyword) + List (inner args) )
type ParseError ¶
type ParseError struct {
Offset int // byte offset into the source where the scanner stopped
Err error // the underlying syntax error
}
ParseError reports a STEP/SPF syntax error together with the byte offset into the source where parsing stopped. Callers can match it with errors.As:
f, err := step.ParseBytes(src)
var pe *step.ParseError
if errors.As(err, &pe) {
log.Printf("bad IFC at byte %d: %v", pe.Offset, pe.Err)
}
I/O errors from ParseFile (e.g. a missing file) are returned unwrapped, so errors.Is(err, os.ErrNotExist) still works — only syntax errors are ParseErrors.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type Scanner ¶
type Scanner struct {
// contains filtered or unexported fields
}
Scanner is a character-stream lexer over an in-memory STEP source buffer. It is deliberately not line-based: STEP permits a single entity record to span many lines, so tokenization walks the byte stream directly.
func NewScanner ¶
NewScanner returns a Scanner over src. src is retained (not copied); Token.Text values alias it, so callers must not mutate src while scanning.
type Token ¶
Token is one lexical unit. Text is a sub-slice of the source buffer (zero-copy); it is only meaningful for the value-bearing kinds (Ref, String, Enum, Bool, Binary, Int, Float, Keyword) — for operators it holds the operator byte(s).
type TokenKind ¶
type TokenKind uint8
TokenKind classifies a STEP/SPF (ISO 10303-21) lexical token. The set mirrors ifcopenshell's IfcSpfLexer token types, ported to Go.
const ( TokEOF TokenKind = iota TokLParen // ( TokRParen // ) TokComma // , TokEquals // = TokSemi // ; TokDollar // $ unset / null TokStar // * derived-in-supertype placeholder TokRef // #123 (Text excludes the '#') TokString // '...' (Text = raw bytes between the quotes, still escaped) TokEnum // .FOO. (Text = label without the dots) TokBool // .T. / .F. / .U. (Text = T/F/U) TokBinary // "0..." (Text = bytes between the quotes) TokInt // 123 / -4 / +7 TokFloat // 1. / -2.5E-3 TokKeyword // IFCWALL, ISO-10303-21, HEADER, DATA, ENDSEC ... )
type TraverseOrder ¶
type TraverseOrder int
TraverseOrder selects the visitation order of File.Traverse.
const ( DepthFirst TraverseOrder = iota // pre-order depth-first (default) BreadthFirst // level-order breadth-first )
type Value ¶
type Value struct {
Str string // KindString / KindEnum / KindBinary / KindTyped(keyword)
List []Value // KindList / KindTyped(inner args)
Ref *Instance // KindRef (resolved target; nil if the target is missing)
F float64 // KindFloat
I int64 // KindInt
RefID uint32 // KindRef (target id, pre-resolution)
Kind Kind // variant tag
B bool // KindBool (.T. -> true, .F. -> false); .U. is KindLogical, not this field
}
Value is a parsed STEP attribute value: an eager, in-memory tagged union (ported from ifcopenshell's attribute-value variant). It is a plain struct, not a boxed interface, so the millions of values in a large model avoid per-value heap allocation. Only the field(s) named for a Kind carry meaning.
Fields are ordered largest-alignment-first so the struct packs to 72 bytes (vs 80 with a naive layout) — a ~10% cut on the dominant memory cost of a big model.