# Extending Layer 5b — adding a new element decoder
This guide walks through adding a decoder for a Revit element class
(Wall, Floor, Door, Window, Column, etc.) to `rvt-rs`. Layer 5a is
already done (ADocument walker); Layer 5b is "per-class decoders
that produce typed element values from the object graph."
**Time required per class**: 30–90 minutes for an experienced Rust
programmer plus access to the `phi-ag/rvt` corpus via `_corpus/`
(fetched by CI with LFS; can be cloned locally the same way).
**Prerequisites**:
- Read `src/walker.rs` to understand `InstanceField` + `decode_instance`
- Read `src/elements/level.rs` — it's the reference example this
guide walks through
- Clone the corpus: `git clone --recurse-submodules
https://github.com/phi-ag/rvt _corpus` in the repo root
- `cargo test --release --test field_type_coverage` passes (sanity
check that your corpus is good)
---
## The three-file change
Adding a decoder for `MyClass` touches exactly three files:
```
src/elements/mod.rs # register the decoder
src/elements/my_class.rs # decoder impl + typed view + tests
tests/elements.rs # integration test against corpus
```
### Step 1 — Check the schema
Before writing any code, confirm the class exists in Revit's
schema and inspect its fields:
```bash
cargo run --release --bin rvt-schema -- _corpus/examples/Autodesk/racbasicsamplefamily-2024.rfa \
| grep -A 20 "class MyClass"
```
Output lists the fields in schema order with their decoded
`FieldType`. Record the field list — you'll use it for the
`synth_schema` helper in the tests.
If `MyClass` doesn't appear, that's useful: it means the class is
out of scope for family files and you may need a project `.rvt` in
the corpus. Check the discovery scripts under `tools/`.
### Step 2 — Create the decoder file
Copy `src/elements/level.rs` as the starting template. Rename
everything (`LevelDecoder` → `MyClassDecoder`, `Level` → `MyClass`,
`"Level"` → `"MyClass"`). Shape:
```rust
//! MyClass — one-line description of the Revit class.
//!
//! | m_name | String | display name |
//! | m_foo | Primitive u32 | some value |
//! | ... | ... | ... |
use crate::formats;
use crate::walker::{DecodedElement, ElementDecoder, HandleIndex, InstanceField};
use crate::{Error, Result};
pub struct MyClassDecoder;
impl ElementDecoder for MyClassDecoder {
fn class_name(&self) -> &'static str { "MyClass" }
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != "MyClass" {
return Err(Error::BasicFileInfo(format!(
"MyClassDecoder received wrong schema: {}", schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MyClass {
pub name: Option<String>,
pub foo: Option<i64>,
// ... typed fields you want to expose
}
impl MyClass {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self { name: None, foo: None };
for (field_name, value) in &decoded.fields {
let n = crate::elements::level::normalise_field_name(field_name);
match (n.as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("foo", InstanceField::Integer { value, .. }) => out.foo = Some(*value),
_ => {}
}
}
out
}
}
```
### Step 3 — Register in the dispatch table
```rust
// src/elements/mod.rs
pub mod level;
pub mod my_class; // <-- add this
pub fn all_decoders() -> Vec<Box<dyn ElementDecoder>> {
vec![
Box::new(level::LevelDecoder),
Box::new(my_class::MyClassDecoder), // <-- and this
]
}
```
The `decoder_class_names_are_unique` test in `src/elements/mod.rs`
will fail at build time if you accidentally register two decoders
for the same class — a useful tripwire.
### Step 4 — Write down how it can fail
Unit tests are banned in this repository, synthesized-byte tests
included. Before writing the decoder, list the ways it could go wrong
on real files: a field that is absent on one release, a record of
another class that happens to parse, a length that runs past the
stream, a value that is plausible but wrong. Each one becomes a check
in the corpus step below, against a real file and Revit's own export
of it, and the decoder returns an `Error` or nothing (fail closed) in
each case.
### Step 5 — Corpus integration test
Corpus tests verify it works on bytes Revit actually writes — the real test of whether your synth matches
reality.
Add to `tests/elements.rs` (create if not present):
```rust
#[test]
#[cfg_attr(not(feature = "corpus"), ignore)]
fn myclass_decodes_on_2024_sample() {
let path = "_corpus/examples/Autodesk/racbasicsamplefamily-2024.rfa";
let mut rf = rvt::RevitFile::open(path).unwrap();
let formats = rf.read_stream("Formats/Latest").unwrap();
let formats_d = rvt::compression::inflate_at(&formats, 0).unwrap();
let schema = rvt::formats::parse_schema(&formats_d).unwrap();
let myclass = schema.classes.iter().find(|c| c.name == "MyClass")
.expect("MyClass present in 2024 schema");
// ... walk to the MyClass instance in Global/Latest (Layer 5a
// is still under development for non-ADocument classes — for
// now, just confirm the schema class has sane fields)
assert!(!myclass.fields.is_empty());
}
```
### Step 6 — Document in `CHANGELOG.md`
Add to the `[Unreleased]` section:
```markdown
### Added
- **`elements::my_class::MyClassDecoder`** — Layer 5b decoder for
Revit's `MyClass` (description). Produces
`elements::my_class::MyClass { name, foo, ... }` as the typed
view; generic `DecodedElement` via `decode_instance` is the
underlying primitive. Covers N fields. Unit-tested with
synthesized bytes; corpus integration test pending live walker
integration (tracked in L5B-01..02).
```
### Step 7 — Commit + PR
- `cargo fmt --all`
- `cargo clippy --all-targets -- -D warnings`
- `tools/check-local.sh` (fmt, clippy, rustdoc, build, real-file checks)
- Commit message: `feat(walker): Layer 5b MyClass decoder (#NNN)`
— reference the GitHub issue (CLASS-xx / M3-xx in `TODO.md`)
- Open PR referencing the issue
---
## Field-name normalisation
Revit uses `m_` prefixes + varies between camelCase / snake_case /
UPPERCASE across versions. `src/elements/level.rs` exposes a
helper (`normalise_field_name`) you should reuse:
```
m_LevelTypeId → leveltypeid
m_level_type_id → leveltypeid
levelTypeId → leveltypeid
LEVEL_TYPE_ID → leveltypeid
```
Match against the normalised form in `from_decoded`. This is one
of the few places where "be generous in what you accept" is the
right posture.
---
## Type-dispatch cheat sheet
Field schema types → `InstanceField` variants:
| Schema field | Wire bytes | `InstanceField` variant |
|---|---|---|
| `Primitive { kind: 0x01, size: 1 }` | 1-byte padded bool | `Bool(bool)` |
| `Primitive { kind: 0x02, size: 2 }` | u16 LE | `Integer { signed: false, size: 2, .. }` |
| `Primitive { kind: 0x04/0x05, size: 4 }` | u32/i32 LE | `Integer { signed: ?, size: 4, .. }` |
| `Primitive { kind: 0x06, size: 4 }` | f32 LE | `Float { size: 4, .. }` |
| `Primitive { kind: 0x07, size: 8 }` | f64 LE | `Float { size: 8, .. }` |
| `Primitive { kind: 0x0b, size: 8 }` | i64 LE | `Integer { signed: true, size: 8, .. }` |
| `String` | u32 count + UTF-16LE chars | `String(String)` |
| `Guid` | 16 raw bytes | `Guid([u8; 16])` |
| `ElementId` / `ElementIdRef` | u32 tag + u32 id | `ElementId { tag, id }` |
| `Pointer { .. }` | u32 + u32 pointer pair | `Pointer { raw: [u32; 2] }` |
| `Vector { .. }` / `Container { .. }` | u32 count + elements | `Vector(..)` or `Bytes(..)` |
---
## When the decoder doesn't fit the generic pattern
Some classes need custom handling beyond `decode_instance`'s
field-by-field walk. Examples:
- **CurtainWall + CurtainGrid**: entity is a graph — the generic
walker can decode the root but following the grid-mullion-panel
references needs `HandleIndex` lookups.
- **FamilyInstance**: the field layout varies by the referenced
Symbol's family. Requires type-aware dispatch.
- **View subtypes** (FloorPlan, Section, etc.): polymorphic — one
base class with several concrete subclasses.
For these, override `decode` and call into `decode_instance` for
the common fields, then manually walk the `fields` vec + do
handle resolution for the specific relationships.
---
## Questions / unknowns
Document them explicitly in the decoder's module docstring, not
buried in comments. Example:
```rust
//! **Known open questions:**
//!
//! - `m_phaseCreated` semantics on R2026 — appears to be a
//! PhaseRef but the ID space is different. Tracked as
//! `docs/rvt-moat-break-reconnaissance.md` §Q7.X.
//! - `m_designOption` is `0xFFFFFFFF` on all files in the sample
//! corpus; unclear what non-default value looks like. Need a
//! project `.rvt` with multiple design options for validation.
```
This way future contributors can see what's uncertain without
reading git blame.
---
## Priority classes for initial adoption
If you're picking which class to tackle, the value-ordered list:
1. **Level** — already done, the reference
2. **Grid** + **ReferencePlane** — similar shape, small surface
3. **Material** + **FillPattern** + **LinePattern** — used by every visible element
4. **Category** + **Subcategory** — foundation for IFC category mapping
5. **View** subtypes (FloorPlan, Section, etc.) — required for sheet rendering
6. **Wall** + **WallType** — first "real geometry" class; unlocks IFC emission
7. **Floor** + **Roof** + **Ceiling** — boundary-sketch-based elements
8. **FamilyInstance** — parent of Door, Window, Furniture, Equipment
9. **Door** / **Window** — host-void semantics, essential for IFC
10. **Column** + **StructuralFraming** — beam/column IFC
11. **Dimension** + **Tag** + **TextNote** — annotation layer
Each unblocks downstream work. The first 6 give you a barebones
exporter + basic viewer; the rest fill in per-category coverage.
---
## Getting help
- Open a GitHub Discussion for "how do I…" questions
- File an Issue with the `class-decoder` label if you hit a
blocker that looks like a bug in the scaffold
- `TODO.md` and `ROADMAP.md` carry the tracked decoder work (the
historical L5B-XX planning ids were never checked in)
- Read `CONTRIBUTING.md` and `CLEANROOM.md` for the source-
provenance rules before contributing