# bim_reader **Repository Path**: xarray/bim_reader ## Basic Information - **Project Name**: bim_reader - **Description**: BIM data reader library, requiring only OpenSceneGraph. NO proprietary dependencies! Currently support .rvt (Revit), .nwc/.nwd (Navisworks), .dwg (AutoCAD) and .ifc formats. - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2026-11-11 - **Last Updated**: 2026-11-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # bim_reader 统一的 BIM 只读解析库:把 **IFC(STEP / ifczip)**、**Navisworks(NWC / NWD / NWF)**、**Revit(RVT / RFA)**、**AutoCAD(DWG)** 读成同一套基于 OpenSceneGraph 的场景结构,供渲染、校验和二次开发使用。 - 只读,不写回任何源文件。 - 所有格式共用一套 API,并且同时支持**文件路径、内存缓冲、`std::istream`** 三种入口(`bim::readFile` / `bim::readMemory` / `bim::readStream`),因此可以直接包成 OSG 的 `osgDB` 插件。 - 场景主数据是 OSG 的 `Group` / `Geometry` / `MatrixTransform`;BIM 属性放在元素的 `UserData` 上。 - 解析失败**失败即跳过并计数**,绝不产出猜测值。 ## 目录 ``` bim_reader/ include/bim/ 对外头文件(BimTypes / BimScene / BimReader / BimValidate) src/core/ 场景构建、属性、校验 src/ifc/ STEP 解析、几何、IFC 读取器 src/nwc/ LcUStream 容器、流解析、NWC/NWD/NWF 读取器、分块解密 src/rvt/ CFB、paged gzip、分区框架、Formats/Latest 类表、ElemTable src/dwg/ DWG 容器(扁平 section locator / page map / Reed-Solomon)、LZ77、handle map、类表与实体解码 tools/ 合成夹具生成脚本、BuiltInParameter 表生成器 tests/ 自包含测试(C++,无测试框架依赖)+ 夹具 data/ r22_schemas.json(Revit 2022 类表)、builtin-parameters/(每年一份 id → 参数名) third_party/ rapidjson(submodule)、rapidxml CMakeLists.txt ``` ## 依赖 | 依赖 | 说明 | |---|---| | OpenSceneGraph ≥ 3.6 | 场景与几何载体。默认 `~/Public/osgverse/build/sdk`,可用 `-DOSG_ROOT=...` 覆盖 | | zlib | NWC/NWD 压缩块、Revit gzip、IFC ifczip | | rapidjson | Revit 类表 JSON、诊断报告(git submodule) | | rapidxml | 预留 XML 支持(本地副本) | 首次克隆后拉取子模块: ```bash git submodule update --init --recursive ``` ## 构建 ```bash cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ -DOSG_ROOT=$HOME/Public/osgverse/build/sdk cmake --build build -j 8 ``` 可选开关: - `-DBIM_BUILD_TESTS=OFF` 不构建测试 - `-DBIM_BUILD_VIEWER=OFF` 不构建 `bim_view` - `-DBIM_DATA_DIR=/path` 指定 `r22_schemas.json` 所在目录 ## 使用 ```bash ./build/bim_info model.ifc # 概要 + 诊断 + 校验 ./build/bim_info model.rvt --elements 20 # 列元素 ./build/bim_info model.nwd --json out.json # 结构化输出 ./build/bim_view model.nwc # 交互查看(OSG) ./build/bim_tests # 单元/回归测试 ``` API 最小示例: ```cpp #include #include // 1) 从路径 bim::ReadResult result = bim::readFile("model.ifc"); // 2) 从内存(缓冲区只需在这次调用期间有效) bim::ReadResult fromMemory = bim::readMemory(bytes.data(), bytes.size(), bim::ReadSource{"model.rvt"}); // 3) 从任意 istream(OSG 插件的入口;name 只用于格式识别与诊断) bim::ReadResult fromStream = bim::readStream(stream, bim::ReadSource{"model.nwc"}); if (result.ok) { /* result.scene.root() 交给 osgViewer */ } ``` `ReadSource::name` 不必真实存在,但请填上扩展名——它用于魔数无法判定时的格式识别、NWF 的兄弟模型解析和错误信息。把 `ReadSource::format` 设成具体格式可跳过探测。 所有长度最终以**米**表示(`ModelInfo::unitToMeter` 记录原单位换算)。 ## 支持矩阵 DWG 的版本范围是**明确**的(见下表);其它版本会带着版本号明确报错,不会按相近版本硬解。 | 能力 | IFC | NWC / NWD / NWF | RVT / RFA | DWG | |---|---|---|---|---| | 容器与目录 | STEP / ZIP | LcUStream(lichunk-007/008) | CFB 复合文档 | R13–R2000 扁平 section locator;R2004+ page-map(R2007 另加 Reed-Solomon) | | 层级/结构树 | ✔ 空间结构 + storey | ✔ 选择树 | ✔ Level → storey(名称与归属已验证) | ✔ 按图层分组 | | 元素与类名 | ✔ | ✔ | ✔ 2022–2027(文件自带类表) | ✔ 每实体一个元素(`LINE #8B` 等),类名来自实体类型 / `AcDb:Classes` | | 属性 | ✔ 属性集 + 类型 | ✔(含环境/external) | ✔ 类名/ClassTag/Base Level/Type Name/Family;四类参数存储 + NamedParam 写进 `Parameters`(id → 名称按文件年份查表) | 图层名 / 颜色索引 / 句柄(写入 `extra` 与元素名) | | 几何网格 | ✔ Brep/Extrusion/Tessellation | ✔ block-index 三角带(NWC / NWD,raw / 打包布局)+ `LcOpNwdFragments` 实例放置(transform / origin) | ✔ GRep 面片(平面 / 圆柱 / 圆锥,含孔洞与圆弧边)+ 族实例(`GInstance` → 符号体 + `InstanceInfo.Trf` 放置) | ✔ 线框(`GL_LINES`)+ `3DFACE` 三角面 | | 单位 | ✔(含 `IFCCONVERSIONBASEDUNIT` 如英尺) | ✔(mm → m) | ✔ | 图纸单位(DWG 未存 `$INSUNITS` 时按 1 单位 = 1 m,`extra["units"]` 说明) | | 压缩块解密 | – | ✔ Blowfish(NWD 记录布局/元数据块) | ✔ 截断 gzip、format 7/9 | ✔ LZ77(AC18 / AC21 两种方言)+ Reed-Solomon(255,239/251) 解交织 | ### DWG 版本与实体 | 版本 | Magic | 容器 | 状态 | |---|---|---|---| | R14 | AC1014 | 扁平 section locator | ✔ 已支持 | | R2000 / 2000i / 2002 | AC1015 | 扁平 section locator | ✔ 已支持 | | R2004 / 2005 / 2006 | AC1018 | page map(0x80 异或头) | ✔ 已支持 | | R2007 / 2008 / 2009 | AC1021 | Reed-Solomon 头 + page map + AC21 LZ77 | ✔ 已支持 | | R2010–2012 / 2013–2017 / 2018+ | AC1024 / AC1027 / AC1032 | page map(同 AC1018) | ✔ 已支持 | | R13 | AC1012 | 扁平 section locator,对象头不同 | ✘ 明确拒绝(未实现) | | R12 及更早 | AC1009 等 | – | ✘ 明确拒绝 | 已解码实体:`LINE`、`POINT`(计数,不出图)、`CIRCLE`、`ARC`、`3DFACE`、`SOLID`(填充四边形)、`LWPOLYLINE`、`POLYLINE_2D` / `POLYLINE_3D`(+ `VERTEX_2D` / `VERTEX_3D` / `SEQEND`)、`LAYER`。`INSERT` / `TEXT` / `MTEXT` / `HATCH` / `SPLINE` / `ELLIPSE` / `MLINE` / `DIMENSION*` / `REGION` / `3DSOLID` / `POLYLINE_PFACE` / `POLYLINE_MESH` 目前计数后跳过,不近似。 已知限制: - RVT 的紧凑参数四个存储(`ParamValueSetDouble/Int/AString/ElementId`)与 `NamedParam` 已能解码,并写进元素的 `Parameters` 属性集:值来自文件,名称来自 `data/builtin-parameters/revit-<年份>.tsv`(2014–2027 每年一份,由 `tools/gen_builtin_parameters.py` 从 NuGet 的 `Revit_All_Main_Versions_API_x64` 读取 `RevitAPI.dll` 枚举元数据生成)。表里没有的 id 保留成 `#-1140285` 这样的原始 id,不猜名字(例如 `-1001101` / `-1001111` 是 Revit 内部参数,官方枚举里没有)。`NamedParam` 同时承载项目/共享参数(id 为正数),其名称在独立的定义表里、尚未解码,只计数(`extra["userParameterValues"]`)。`NamedParam` 的四个值槽没有类型标记,约定按“字符串非空 → 正元素 id → 非零整数 → 非零 double”取第一个实际填写的槽,因此“恰好为 0 的数值”与“未填写”无法区分时按未填写处理(计入 `extra["parameterValuesWithoutValue"]`)。 - RVT 的标高高程(`SpatialDraft::elevation`)尚未解出:`Level` 记录中的高程编码未确认,不做猜测。 - RVT GRep 几何覆盖 `Geometry → Face → EdgeLoop → Edge → EdgePnt` 整张图:平面(`Plane`)、圆柱(`CylSurf`)与圆锥(`ConeSurf`)按各自的 (u, v) 公式求值,多环按“最小包含环 = 孔”桥接后耳切;`Edge.m_interiorEdgePnts` 作为折线中段参与拼接,因此圆弧边(椅子、圆桌、栏杆)能出三角形。族实例(门、窗、家具、楼梯、栏杆)的 `InstanceInfo.m_symbolId + m_Trf` 指向另一个元素的 GRep 体,按“符号体 ∘ 12 个 double 的放置”递归装配;被别的元素当作符号引用的元素只作为几何来源,不单独出图。仍未求值的曲面类(`SurfRev` / `RuledSurf` / `HermiteSurf`)按类名计数跳过(`grepCurved.<类名>`),不近似。 - RVT GRep 的跳过原因都写入 `ModelInfo::extra`,便于回归对比:`grepSkippedFaces` 拆成 `grepSkippedCurved` / `grepSkippedUnjoined` / `grepSkippedUnresolved`,后者再拆成 `grepSkippedMissingFace` / `grepSkippedNoLoop` / `grepSkippedNoSurface`;另有 `grepUnjoined.<类名>`(按曲面类)、`grepCollapsedLoops`(只有一条闭合边、缺弧几何的环)、`grepMaxLoopGap`(被拒环的最大断口,英尺)、`grepInstanceNodes` / `grepInstances` / `grepInstanceMissing` / `grepInstanceEmbedded` / `grepInstanceCycle` / `grepInstanceDeep` / `grepInstanceUnplaced`。 - RVT 的 `Face.m_pSurf == 0`(Revit 的备用/派生面)没有可求值的曲面,计入 `grepSkippedNoSurface`;2022–2027 的 house/office 里跳过面全部是这一类,墙面本身是闭合的。 - 2024+ 的文件里,圆柱面侧面仍有少量环因接缝(u 相差 2π)未能闭合,计入 `grepUnjoined.CylSurf`;这类面不做近似三角化。 - RVT 的材质 / 着色样式尚未解码,几何颜色按构件类型取统一调色板(与 IFC 的 `paletteFor` 一致:墙米色、板灰、屋顶砖红、门窗有色)。因此 RVT 出图是“按类着色”而非文件里的真实材质。 - NWD/NWC 几何覆盖 `LcOpNwdGeometry` 的三角带记录(type 94),并由 `LcOpNwdFragments` 提供每个实例的放置(transform 14–17 或几何记录的 origin)。线(95)、点(96)与外部/参数化几何(181/103/104/105)目前只跳过不建网格。每个模型的记录编码由 `LcOpNwdGeometryCompress` 决定(坐标/法线/颜色/UV/长度/索引各自 raw 或打包),raw 与调色板两种布局都支持。 - NWD 的单位目前固定按毫米(`unitScale=0.001`);发布单位是米的分区(如 `bathcity`、`ice stadium`)尺寸会小 1000 倍,分区 linear unit 字段尚未解码。以米为单位的模型请以元素相对关系为准。 - 片段流里出现 protein(着色器)资源时不会解码该资源,而是用完整校验向后定位几何引用;整条流必须恰好消费完,否则回退到“几何按原点摆放”并给出 `nwc.fragments` 警告(例如 `bathcity/north.nwd`)。 - NWF 的模型引用从 `LcOpNwfSceneSet` 解析(名称 + 原始路径),`LcOpNwfPathMap` 作为无 scene-set 时的回退;被引用的 NWC/NWD 会合并进同一场景。若 NWF 只引用 DWG/3DS/DWF 等本库不支持的原生格式,会以 `no referenced model could be loaded` 明确失败,不伪造几何。 - 未被识别的支撑记录(`*Attributes`、`*Elem`、`*Type`、`Family*`、`StairsPath*`、`RebarShape*` 等)统一归为 `Proxy`,不计入墙/板/门/窗。 - DWG 支持 AC1014–AC1032(见上表),R13 (AC1012) 与 R12 及更早明确拒绝。page-map 两条路径(R2004 家族、R2007)都按容器声明的 section 大小校验:解出来短于声明会报 `dwg.container` 错误,不补零伪造。 - DWG 的单位仍是“图纸单位”:文件里的 `$INSUNITS` 在 `AcDb:Header` 中,尚未解码,因此统一按 1 单位 = 1 m(`extra["units"]` 说明)。 - DWG 的 `INSERT`(块引用)暂不展开:块定义里的实体已经解码,但块基点存放在 `BLOCK_HEADER` 而该对象尚未解,缺少基点就无法诚实地构造放置变换,因此 INSERT 只计数并告警。`TEXT` / `MTEXT` / `HATCH` / `SPLINE` / `ELLIPSE` / `MLINE` / `DIMENSION` / `REGION` / `3DSOLID` / `POLYLINE_PFACE` / `POLYLINE_MESH` 同样只计数后跳过。 - DWG 的 `POINT` 只解出坐标(`pointsWithoutGeometry`),不生成点云或十字标记——那属于渲染选择,不是文件内容。 - DWG 的 `LWPOLYLINE` / `POLYLINE` 的 bulge(圆弧段)按弦高采样成线段,采样密度由 `ReadOptions::curveDeflection` 控制,与圆/弧一致。 ### DWG 三个参考实现的对照 三个只读参考实现各有侧重,本库按“能交叉验证的才写进代码”的原则取用: | 维度 | ACadSharp(C#, MIT) | GNU LibreDWG(C) | dwg-rs(Rust) | |---|---|---|---| | R2004 容器 | `DwgReader.readFileHeaderAC18`:0x80 处 0x6C 字节与魔数异或,page map / section map 两级 | `decode_r2007.c` | `header.rs` + `section_map.rs`,system page 头是明文、数据页头 XOR | | R2007 容器 | `readFileHeaderAC21`:RS(255,239)×3 + AC21 LZ → 0x110 头;page map / section map 用 239 字节消息、数据页 251 | 同上(`reed_solomon` 只解交织) | `r2007.rs`,同一套常量 | | LZ77 | `DwgLZ77AC18/AC21Decompressor` | `decompress_R2007` | `lz77.rs` / `r21_lz.rs`(AC21 的 32 条置换表最清晰) | | 对象头 | `DwgObjectReader.getEntityType`:R2000–R2007 类型后跟 `RL bitsize`;R2010+ 改为 `MC handleBits` + 2 位分派的对象类型 | `decode.c` | `object.rs`,与 ACadSharp 一致 | | R2007+ 字符串流 | `SetPositionByFlag` + `DwgMergedReader`(数值走主读者、字符串走文本读者) | `str_dat` 分离 | `string_stream.rs`,同一套 flag + **小端** 16 位大小 | | handle map | `DwgHandleReader`:段大小 `== 2` 结束(末段只有 CRC) | 同 | 段大小 `== 0` 结束;尾部带非零填充的文件会报段越界 | 结论与取舍: - 容器与压缩层用 **ACadSharp + dwg-rs 双向对照**(两者字段偏移逐项一致):R2004 家族一条路径,R2007 用 Reed-Solomon 解交织 + AC21 LZ77。 - 对象层用 **libredwg + ACadSharp 对照**:R2010+ 的 `MC + 2 位类型分派`、`handleSectionOffset = payloadStart + size*8 - handleSize`、`AcDb:Classes` 各版本的前置字段与 4 个额外 `BL`。 - 一处已知分歧:R2007+ 实体 common data 里 material / shadow / plotstyle 的顺序,ACadSharp 是 material → shadow → plotstyle,libredwg 是 material → plotstyle → shadow。两者读到的总位数相同,几何不受影响;本库按 libredwg 的顺序(与原实现一致)。 - R2007+ 的字符串流大小字段是**小端 ushort**(`ReadUShort`),不是位短整型;按大端读会把字符串流起点算到别的对象里(表现为图层名乱码或整段为空)。 - 三方都不支持的(R13 对象头、加密 section、密码保护文件)明确拒绝,不做近似。 ## 测试 ```bash ./build/bim_tests ctest --test-dir build --output-on-failure ``` 测试夹具在 `tests/data/`: - `sample.ifc` / `sample.ifczip` / `sample.nwc` / `sample.nwd` / `sample.nwf` / `sample.rvt`(format 9)/ `sample-format7.rvt`(旧分区框架) - `rvt-corpus/`:rvt-rs 生成的 tier1 夹具(Apache-2.0) 回归数据建议放在仓库外,用参数传入,例如: ```bash ./build/bim_info "/path/to/rvt-holdout-models-main/2024/rvt-holdout-2024.rvt" ``` RVT 几何的交叉验证方法:同名 IFC 里的 `Qto_*BaseQuantities`(`Width/Height/Length/Depth/ProjectedArea`)来自同一个 Revit 模型,可作为 oracle。当前 2022–2027 的 house/office 共 113 个墙/板/屋顶全部通过:墙厚度/高度误差 < 0.01 m、板三维尺寸误差 < 0.01 m、屋顶每层投影面积等于 IFC `ProjectedArea`;带孔楼板两个面的投影面积精确等于 2×(96−6.58) m²。元素标高名称也已写入场景,与 IFC `IfcRelContainedInSpatialStructure` 对照 0 分歧。 RVT 内置参数命名覆盖率(`extra["parameterValuesNamed"]` 命中表 / `parameterValueIdsUnknown` 未命中):2024 house 19,376 / 146(99.3%)、2024 office 19,311 / 144、2022 house 10,999 / 137;2014 轨道项目 294,361 / 0(该版本用到的 id 全部在 2014 表里)。未命中的 id 以 `#-1140285` 形式保留,不回退到别的年份的表去猜。 族实例按元素包围盒与 IFC 对照(IFC 元素的 `Tag` 就是 Revit 元素 id):10 个 house/office 中按 Tag 能配上的 31–33 个元素里,78–88% 的包围盒与 IFC 完全一致(差值 < 0.01 m,门/窗/家具/楼梯都包含在内),其余差异都来自几何内容而不是放置——例如外墙末端多出的压顶/窗台(0.13–0.16 m)、门扇上 Revit 自带的两根细长构造条(把包围盒撑到 2 m)。 DWG 的交叉验证用**同一张图纸的不同版本存储**互为 oracle(比单个文件更能暴露容器错误),并用 GNU LibreDWG 的 handle → offset 全量比对: - `libredwg/test/test-data` 的 `example_r14/2000/2004/2007/2010/2013/2018.dwg` 是同一张图的 7 种存储,本库输出**逐项一致**:103 个元素 / 12,333 条线段 / 19 个三角面、4 个图层、`objectsMalformed=0`、`offsetsWithoutRecord=0`;`sample_2000` 与 `sample_2018` 同样一致(5 元素 / 78 线段)。 - `ACadSharp/samples` 的 `sample_AC1014…AC1032.dwg` 也是同一张图的 7 种存储,输出全部一致:153 个元素 / 935 条线段 / 18 个三角面、5 个图层、包围盒完全相同、`objectsMalformed=0`;`sample_base/sample_base.dwg` 得到同样的 153/935/18。 - `ACadSharp/samples/aec_objects/AecObjects.dwg`(R2018,AEC 对象):1,573 个对象全部解析(0 malformed、0 个 offset 无记录),478 个类;AEC 专有对象按类型跳过,6 个普通实体正常出图。 - `ACadSharp/samples/dynamic-blocks/*.dwg`(10 个动态块样例)全部可读,0 malformed。 - 真实图纸(`~/Public/Assets/BIM/`):`Gatehouse.dwg`(AC1015)100,355 个对象 → 42,172 个元素 / 0 malformed;`Library.dwg`(AC1015)20,001 个对象 → 148 个元素 / 276 个三角面;`c12_Meadowgate_{Civil,Ductwork,Structure,Architecture}.dwg`(**AC1021**)分别为 148,684 / 8,822 / 7,794 / 6,494 个对象,全部 0 malformed,产出 8,546 / 12,523 / 118 / 700 条线段与 5 / 2,331 / 12,634 / 2,793 个三角面;`Chair.dwg`(AC1027)231 个对象 0 malformed,但几何全在块定义里(5 个 `INSERT`),因此在 `INSERT` 展开前元素数为 0——这是已知限制的真实案例,不是解析失败。 测试夹具里没有 DWG(体积与版权原因),DWG 的回归请把文件路径传给 `bim_info`;压缩与解交织的位级契约由 `tests/test_main.cpp::testDwgCompression` 用合成流固定。 ## 路线图 1. **DWG R13 (AC1012)** 的对象头:与 R14 的差异未逐一确认,暂不按相近版本硬解; 2. **DWG 块引用**:解 `BLOCK_HEADER`(名字 + 基点)与 `INSERT` 的 `block_header` 句柄,展开块内实体并应用插入变换; 3. **DWG 单位**:解 `AcDb:Header` 的 `$INSUNITS`,把图纸单位换算成米; 4. **OSG 插件**:把 `bim::readStream` 包成 `osgDB` 的 `ReaderWriter`,让 `osgDB::readNodeFile("model.rvt")` 直接可用; 5. NWD 单位(分区 linear unit)与线(95)/点(96)几何; 6. RVT GRep 其余曲面求值(`SurfRev` / `RuledSurf` / `HermiteSurf`)与圆柱接缝环; 7. RVT 标高高程; 8. **Tekla**:`.db1` 模型(暂无测试数据,暂缓); 9. 空间树与楼层归属(RVT Level、NWD 层、IFC storey 统一)。 ## 许可证 MIT,见 [LICENSE](LICENSE)。