rvt 0.4.0

Apache-2.0 clean-room reader for Autodesk Revit files (.rvt, .rfa, .rte, .rft) without Revit: metadata, previews and the embedded schema of 2016-2026 files, and typed elements with measured geometry from Revit 2024 and 2025 projects, exported as IFC4, glTF and plan SVG.
Documentation
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# rvt-rs — Python quickstart\n",
    "\n",
    "Walk through the most common uses of the `rvt` Python bindings: open a Revit file, inspect metadata, read the root document via the schema-directed walker, and export a document-level IFC4 scaffold.\n",
    "\n",
    "**Install**: `pip install rvt` once the wheel is published; from a source checkout run `maturin build --release --features python && pip install target/wheels/rvt-*.whl`.\n",
    "\n",
    "**Sample file**: this notebook uses `racbasicsamplefamily-2024.rfa` from the public `rac_basic_sample_family` corpus Autodesk ships with every Revit release. The repo's `samples/` directory (and the `RVT_SAMPLES_DIR` env var honoured below) is where the integration tests look for it. Any `.rvt`/`.rfa`/`.rte`/`.rft` file works — swap the path in cell 1."
   ],
   "id": "cell-00"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import pathlib\n",
    "\n",
    "import rvt\n",
    "\n",
    "print(f\"rvt version: {rvt.__version__}\")\n",
    "\n",
    "# Sample-path resolution mirrors tests/python/test_rvt.py:\n",
    "#   1. honour the RVT_SAMPLES_DIR env var if set\n",
    "#   2. otherwise try a handful of sensible paths relative to the\n",
    "#      notebook's working directory\n",
    "#\n",
    "# The in-repo `samples/` directory lives at the monorepo root in the\n",
    "# canonical checkout (../../../samples when this notebook is run from\n",
    "# <rvt-rs>/docs/). Point RVT_SAMPLES_DIR at any directory holding\n",
    "# `racbasicsamplefamily-*.rfa` to use your own corpus.\n",
    "def resolve_samples_dir() -> pathlib.Path:\n",
    "    env_dir = os.environ.get(\"RVT_SAMPLES_DIR\")\n",
    "    if env_dir:\n",
    "        return pathlib.Path(env_dir).expanduser().resolve()\n",
    "    here = pathlib.Path.cwd().resolve()\n",
    "    candidates = [\n",
    "        here / \"samples\",\n",
    "        here.parent / \"samples\",\n",
    "        here.parent.parent / \"samples\",\n",
    "        here.parent.parent.parent / \"samples\",\n",
    "    ]\n",
    "    for c in candidates:\n",
    "        if c.exists():\n",
    "            return c\n",
    "    return candidates[-1]\n",
    "\n",
    "samples_dir = resolve_samples_dir()\n",
    "SAMPLE = samples_dir / \"racbasicsamplefamily-2024.rfa\"\n",
    "print(f\"Samples dir : {samples_dir}\")\n",
    "print(f\"Sample file : {SAMPLE.name}\")\n",
    "print(f\"Exists      : {SAMPLE.exists()}\")\n",
    "if not SAMPLE.exists():\n",
    "    print()\n",
    "    print(\"To run this notebook, set RVT_SAMPLES_DIR to a directory\")\n",
    "    print(\"containing Autodesk's public `rac_basic_sample_family` corpus,\")\n",
    "    print(\"or point SAMPLE at any .rvt/.rfa/.rte/.rft file you have locally.\")"
   ],
   "id": "cell-01"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Open the file and inspect metadata\n",
    "\n",
    "Constructing `RevitFile(path)` opens the MS-CFB container and keeps the bytes cached in memory. The metadata getters (`version`, `build`, `guid`, `part_atom_title`, `original_path`) parse on-demand — nothing heavy runs until you call a method that actually touches a stream. Size limits default to 2 GiB file / 256 MiB stream / 256 MiB inflate; override via `max_file_bytes=`, `max_stream_bytes=`, `max_inflate_bytes=` keyword arguments on the constructor if you need to cap the reader tighter for hostile input."
   ],
   "id": "cell-02"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = rvt.RevitFile(str(SAMPLE))\n",
    "\n",
    "print(f\"Revit release  : {f.version}\")\n",
    "print(f\"Build tag      : {f.build}\")\n",
    "print(f\"Document title : {f.part_atom_title}\")\n",
    "print(f\"Document GUID  : {f.guid}\")\n",
    "print(f\"Original path  : {f.original_path}\")\n",
    "print(f\"repr()         : {f!r}\")"
   ],
   "id": "cell-03"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Full `BasicFileInfo` as JSON\n",
    "\n",
    "`basic_file_info_json()` returns the entire `BasicFileInfo` record as a JSON string. It is the single-call equivalent of the four getters above (`version`, `build`, `guid`, `original_path`) plus any future fields the Rust `BasicFileInfo` struct grows — `json.loads()` gives you a plain dict. Returns `None` if the `BasicFileInfo` stream can't be parsed."
   ],
   "id": "cell-04"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "bfi_json = f.basic_file_info_json()\n",
    "if bfi_json is None:\n",
    "    print(\"BasicFileInfo stream unreadable; metadata getters will also return None\")\n",
    "else:\n",
    "    bfi = json.loads(bfi_json)\n",
    "    print(json.dumps(bfi, indent=2))"
   ],
   "id": "cell-05"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. List the OLE streams\n",
    "\n",
    "A Revit file is an MS-CFB container holding a set of named streams. The 2016–2026 releases all carry the same 12 invariant streams plus a version-specific `Partitions/NN` marker. `missing_required_streams()` is the cheapest way to pre-validate an input before running heavy extractors — an empty list means the file at least looks structurally like Revit."
   ],
   "id": "cell-06"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for s in f.stream_names():\n",
    "    print(f\"  {s}\")\n",
    "\n",
    "missing = f.missing_required_streams()\n",
    "if missing:\n",
    "    print(f\"\\nmissing required streams: {missing}\")\n",
    "else:\n",
    "    print(\"\\nall required streams present\")"
   ],
   "id": "cell-07"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Schema inventory\n",
    "\n",
    "The `Formats/Latest` stream carries Revit's complete serialisation schema: every class name, every field, every C++ type signature. `schema_summary()` is cheap (counts only); `schema_json()` is the full dump and runs around 1–2 MB for a typical family. We print the summary plus the first few class names from the full dump to keep the output readable."
   ],
   "id": "cell-08"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "summary = f.schema_summary()\n",
    "print(f\"Classes in this file   : {summary['classes']}\")\n",
    "print(f\"Total declared fields  : {summary['fields']}\")\n",
    "print(f\"Distinct C++ type sigs : {summary['cpp_types']}\")\n",
    "\n",
    "schema = json.loads(f.schema_json())\n",
    "print(f\"\\nFirst 10 class names (of {len(schema['classes'])}):\")\n",
    "for cls in schema['classes'][:10]:\n",
    "    print(f\"  tag={cls['tag']:>5}  fields={len(cls['fields']):>3}  {cls['name']}\")\n",
    "\n",
    "# Confirm ADocument is present — it is the root class the Layer-5a\n",
    "# walker targets in the next cell.\n",
    "adoc = next((c for c in schema['classes'] if c['name'] == 'ADocument'), None)\n",
    "print(f\"\\nADocument found: {adoc is not None}  \"\n",
    "      f\"(fields={len(adoc['fields']) if adoc else 'n/a'})\")"
   ],
   "id": "cell-09"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Read the root `ADocument` record\n",
    "\n",
    "`read_adocument()` runs the Layer-5a walker and returns `ADocument`'s declared instance fields as a Python dict, or `None` if the entry-point detector can't confidently locate the record. Releases 2024–2026 are cross-version validated: the last three `ElementId` fields (`m_ownerFamilyId`, `m_ownerFamilyContainingGroupId`, `m_devBranchInfo`) are byte-identical across those releases on the reference corpus. Releases 2016–2023 decode the schema in full but the walker currently reports `partial` on them — see `docs/compatibility.md` for the matrix.\n",
    "\n",
    "Per-field decoding beyond `ADocument` (walls, doors, levels, families, etc.) is implemented on the Rust side; exposing it to Python is tracked separately (see task #147 / `ADocumentInstance` completeness markers)."
   ],
   "id": "cell-10"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "doc = f.read_adocument()\n",
    "if doc is None:\n",
    "    print(\"walker couldn't locate ADocument in this file\")\n",
    "else:\n",
    "    print(f\"ADocument at offset 0x{doc['entry_offset']:06x}  (Revit {doc['version']})\")\n",
    "    print()\n",
    "    for i, field in enumerate(doc['fields']):\n",
    "        kind = field['kind']\n",
    "        name = field['name']\n",
    "        detail = ''\n",
    "        if kind == 'pointer':\n",
    "            detail = f\"slots [{field['slot_a']:#x}, {field['slot_b']:#x}]\"\n",
    "        elif kind == 'element_id':\n",
    "            detail = f\"tag={field['tag']}, id={field['id']}\"\n",
    "        elif kind == 'ref_container':\n",
    "            detail = f\"count={field['count']}\"\n",
    "        elif kind == 'bytes':\n",
    "            detail = f\"len={field['len']}\"\n",
    "        print(f\"  [{i:>2}] {name:<36} {kind:<14}  {detail}\")"
   ],
   "id": "cell-11"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Export to IFC4 (document-level)\n",
    "\n",
    "`write_ifc()` produces a spec-valid IFC4 STEP text: the ISO-10303-21 envelope, the required framework entities (`IfcPerson` / `IfcOrganization` / `IfcApplication` / `IfcOwnerHistory` / `IfcSIUnit` / `IfcUnitAssignment` / `IfcGeometricRepresentationContext`), and an `IfcProject` populated from the document metadata.\n",
    "\n",
    "**Coverage is document-level only.** Building-element geometry (walls, doors, floors, roofs, etc.) is not yet emitted — that pipeline depends on per-class element decoders currently exposed only through the Rust API. See `docs/compatibility.md` §IFC for the current scope and `CONTRIBUTING.md` → *Where help is most wanted* if you want to extend coverage."
   ],
   "id": "cell-12"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ifc_text = f.write_ifc()\n",
    "print(f\"IFC output length : {len(ifc_text):,} bytes\")\n",
    "print(f\"IFC4 schema       : {'FILE_SCHEMA(' in ifc_text and 'IFC4' in ifc_text}\")\n",
    "print(f\"IfcProject count  : {ifc_text.count('IFCPROJECT(')}\")\n",
    "\n",
    "# Write the STEP text next to the input so you can inspect it with\n",
    "# any IFC viewer / validator (e.g. IfcOpenShell's `IfcConvert`).\n",
    "out_path = pathlib.Path(str(SAMPLE).replace('.rfa', '.ifc'))\n",
    "out_path.write_text(ifc_text)\n",
    "print(f\"Wrote             : {out_path}\")\n",
    "\n",
    "print()\n",
    "print(\"First 20 lines of the STEP file:\")\n",
    "for line in ifc_text.splitlines()[:20]:\n",
    "    print(f\"  {line}\")"
   ],
   "id": "cell-13"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Cross-version sanity check\n",
    "\n",
    "If you have multiple `racbasicsamplefamily-*.rfa` releases on disk, this cell opens each and prints the reported Revit release year. The 2016–2019 files use the older `rac_basic_sample_family-<year>.rfa` naming; 2020+ use the compact `racbasicsamplefamily-<year>.rfa` form. Skip this cell if you only have the one sample."
   ],
   "id": "cell-14"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "candidates = sorted(samples_dir.glob('rac*sample*family*.rfa')) if samples_dir.exists() else []\n",
    "\n",
    "if not candidates:\n",
    "    print(f\"No corpus files found under {samples_dir}\")\n",
    "else:\n",
    "    for path in candidates:\n",
    "        try:\n",
    "            g = rvt.RevitFile(str(path))\n",
    "            print(f\"  {path.name:<45} Revit {g.version}  title={g.part_atom_title!r}\")\n",
    "        except Exception as exc:\n",
    "            print(f\"  {path.name:<45} <open failed: {exc}>\")"
   ],
   "id": "cell-15"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Where to next?\n",
    "\n",
    "- **Compatibility matrix**: [`docs/compatibility.md`](compatibility.md) — release-by-release breakdown of what parses vs. what the walker decodes vs. what the IFC exporter emits. Source-of-truth for current limitations.\n",
    "- **Python API reference**: [`docs/python.md`](python.md) — full surface of the `rvt` module with return shapes.\n",
    "- **Recon report**: [`docs/rvt-moat-break-reconnaissance.md`](rvt-moat-break-reconnaissance.md) — the living technical record of the Revit on-disk format as we've reverse-engineered it clean-room.\n",
    "- **Contributing**: [`CONTRIBUTING.md`](../CONTRIBUTING.md) — especially the *Where help is most wanted* section if you want to extend the walker or IFC coverage.\n",
    "- **Issues**: [github.com/DrunkOnJava/rvt-rs/issues](https://github.com/DrunkOnJava/rvt-rs/issues)\n",
    "\n",
    "### Current limitations (summary)\n",
    "\n",
    "- **Walker coverage**: `ADocument` root fields decoded across all 11 releases; fully cross-version-validated on 2024–2026. Per-class element decoders (walls, doors, families, etc.) exist on the Rust side but are not yet exposed through the Python bindings.\n",
    "- **IFC export**: document-level scaffold only. No element geometry, no materials, no property sets yet. Output is schema-valid IFC4 STEP that `IfcOpenShell` will round-trip.\n",
    "- **Write path**: the Rust crate supports a verified write-back round-trip; no Python binding yet."
   ],
   "id": "cell-16"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}