From 60455c23e9ab57437bb61c4f56bdc02dd4cc90e6 Mon Sep 17 00:00:00 2001 From: mahach666 <79989386+mahach666@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:24:10 +0300 Subject: [PATCH 1/5] Read IFC and RVT in the CLI through IfcIO.Scene and RvtIO.Scene --- Directory.Packages.props | 2 + src/IOViewer.Cli/IOViewer.Cli.csproj | 4 + src/IOViewer.Cli/Program.cs | 44 ++++- tests/IOViewer.Tests/CliTests.cs | 2 +- .../ModelAndCliIntegrationTests.cs | 171 ++++++++++++++++++ 5 files changed, 216 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7ef9376..ceb63fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,8 @@ + + diff --git a/src/IOViewer.Cli/IOViewer.Cli.csproj b/src/IOViewer.Cli/IOViewer.Cli.csproj index c13d185..d6cc513 100644 --- a/src/IOViewer.Cli/IOViewer.Cli.csproj +++ b/src/IOViewer.Cli/IOViewer.Cli.csproj @@ -9,10 +9,14 @@ net8.0;net10.0 true false + + NU1901;NU1902;NU1903;NU1904 + + diff --git a/src/IOViewer.Cli/Program.cs b/src/IOViewer.Cli/Program.cs index 29d9984..a399a41 100644 --- a/src/IOViewer.Cli/Program.cs +++ b/src/IOViewer.Cli/Program.cs @@ -1,25 +1,30 @@ using System.Diagnostics; using System.Net; using System.Reflection; +using IfcIO.Scene; +using IOScene; using IOViewer.Exporters; using IOViewer.Importers; using IOViewer.Model; +using RvtIO.Scene; namespace IOViewer.Cli; internal static class Program { private const string UsageText = """ - Usage: ioviewer [output.html] [options] + Usage: ioviewer [output.html] [options] ioviewer --help | --version Generates standalone interactive 3D WebGL / HTML BIM & CAD model viewers - from 3D models (glTF/GLB, Wavefront OBJ, JSON). The HTML file has no - external dependencies and works offline. JSON input is viewer JSON, an - IOScene scene file or an IOViewer 0.1/0.2 snapshot. + from 3D models (glTF/GLB, Wavefront OBJ, JSON, IFC, Revit). The HTML file + has no external dependencies and works offline. JSON input is viewer JSON, + an IOScene scene file or an IOViewer 0.1/0.2 snapshot. IFC (.ifc, .ifczip) + is read with IfcIO and Revit (.rvt) with RvtIO; what they could not convert + is reported as warnings. Arguments: - Path to input 3D model (.glb, .obj, or .json). + Path to input 3D model (.glb, .obj, .json, .ifc, .ifczip or .rvt). [output] Destination path for exported file (.html or .json). Default: .html. @@ -167,7 +172,9 @@ public static int Main(string[] args) ".glb" => GlbViewerImporter.Load(inputPath), ".obj" => ObjViewerImporter.Load(inputPath), ".json" => JsonViewerImporter.Load(inputPath), - _ => throw new NotSupportedException($"Unsupported input file extension: '{ext}'. Supported: .glb, .obj, .json.") + ".ifc" or ".ifczip" => FromIfc(inputPath), + ".rvt" => FromRvt(inputPath), + _ => throw new NotSupportedException($"Unsupported input file extension: '{ext}'. Supported: .glb, .obj, .json, .ifc, .ifczip, .rvt.") }; } catch (Exception ex) @@ -232,6 +239,31 @@ public static int Main(string[] args) return 0; } + private static ViewerDocument FromIfc(string path) + { + var result = IfcSceneReader.Read(path); + return FromScene(path, result.Document, result.Diagnostics); + } + + private static ViewerDocument FromRvt(string path) + { + var result = RvtSceneReader.Read(path); + return FromScene(path, result.Document, result.Diagnostics); + } + + // The engine adapters key the model by its file name, as the other importers do; the title falls back to it too. + private static ViewerDocument FromScene(string path, SceneDocument scene, IReadOnlyList diagnostics) + { + if (string.IsNullOrEmpty(scene.Title)) scene.Title = Path.GetFileNameWithoutExtension(path); + // Information entries (such as an element that has no geometry by design) are not warnings; one line per kind. + foreach (var kind in diagnostics.Where(d => d.Severity != SceneSeverity.Info).GroupBy(d => d.Code)) + { + int more = kind.Count() - 1; + Console.Error.WriteLine($"Warning: {kind.Key}: {kind.First().Message}{(more > 0 ? $" ({more} more)" : "")}"); + } + return new ViewerDocument(scene); + } + private static void StartServe(string htmlPath, int port) { string url = $"http://localhost:{port}/"; diff --git a/tests/IOViewer.Tests/CliTests.cs b/tests/IOViewer.Tests/CliTests.cs index f4fdc30..3c1dad1 100644 --- a/tests/IOViewer.Tests/CliTests.cs +++ b/tests/IOViewer.Tests/CliTests.cs @@ -19,7 +19,7 @@ public void Cli_Help_ReturnsSuccessAndShowsUsage() int code = Program.Main(HelpArgs); Assert.Equal(0, code); string output = sw.ToString(); - Assert.Contains("ioviewer ", output); + Assert.Contains("ioviewer ", output); Assert.Contains("--wireframe", output); Assert.Contains("--white-mode", output); Assert.Contains("--serve", output); diff --git a/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs b/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs index d9dc573..99771de 100644 --- a/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs +++ b/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs @@ -1,6 +1,7 @@ using IOScene; using IOViewer.Cli; using IOViewer.Exporters; +using IOViewer.Importers; using IOViewer.Model; using Xunit; @@ -189,4 +190,174 @@ public void Cli_ConvertsEveryJsonKindToHtml(string kind) if (File.Exists(tempHtml)) File.Delete(tempHtml); } } + + private const string Hostile = "\">"; + + // Project, site, building and storey; a wall box with Pset_WallCommon, a window without a style (drawn + // translucent by its IFC class) and a proxy whose name and property set are hostile strings. Lengths in mm. + private const string SmokeIfc = $""" + ISO-10303-21; + HEADER; + FILE_DESCRIPTION((''),'2;1'); + FILE_NAME('smoke.ifc','2026-01-01T00:00:00',(''),(''),'','',''); + FILE_SCHEMA(('IFC2X3')); + ENDSEC; + DATA; + #1=IFCPROJECT('0SmokeProject000000001',$,'Smoke',$,$,$,$,(#20),#10); + #10=IFCUNITASSIGNMENT((#11)); + #11=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); + #20=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#21,$); + #21=IFCAXIS2PLACEMENT3D(#22,$,$); + #22=IFCCARTESIANPOINT((0.,0.,0.)); + #30=IFCSITE('0SmokeSite000000000001',$,'Site',$,$,#31,$,$,.ELEMENT.,$,$,$,$,$); + #31=IFCLOCALPLACEMENT($,#21); + #32=IFCBUILDING('0SmokeBuilding00000001',$,'Building',$,$,#33,$,$,.ELEMENT.,$,$,$); + #33=IFCLOCALPLACEMENT(#31,#21); + #40=IFCBUILDINGSTOREY('0SmokeStorey0000000001',$,'Level 1',$,$,#41,$,$,.ELEMENT.,0.); + #41=IFCLOCALPLACEMENT(#33,#21); + #50=IFCRELAGGREGATES('0SmokeAggregates000001',$,$,$,#1,(#30)); + #51=IFCRELAGGREGATES('0SmokeAggregates000002',$,$,$,#30,(#32)); + #52=IFCRELAGGREGATES('0SmokeAggregates000003',$,$,$,#32,(#40)); + #100=IFCWALL('1SmokeWall000000000001',$,'Wall',$,$,#101,#102,$); + #101=IFCLOCALPLACEMENT(#41,#21); + #102=IFCPRODUCTDEFINITIONSHAPE($,$,(#103)); + #103=IFCSHAPEREPRESENTATION(#20,'Body','Brep',(#104)); + #104=IFCFACETEDBREP(#105); + #105=IFCCLOSEDSHELL((#110,#111,#112,#113,#114,#115)); + #110=IFCFACE((#120)); + #111=IFCFACE((#121)); + #112=IFCFACE((#122)); + #113=IFCFACE((#123)); + #114=IFCFACE((#124)); + #115=IFCFACE((#125)); + #120=IFCFACEOUTERBOUND(#130,.T.); + #121=IFCFACEOUTERBOUND(#131,.T.); + #122=IFCFACEOUTERBOUND(#132,.T.); + #123=IFCFACEOUTERBOUND(#133,.T.); + #124=IFCFACEOUTERBOUND(#134,.T.); + #125=IFCFACEOUTERBOUND(#135,.T.); + #130=IFCPOLYLOOP((#200,#203,#202,#201)); + #131=IFCPOLYLOOP((#204,#205,#206,#207)); + #132=IFCPOLYLOOP((#200,#201,#205,#204)); + #133=IFCPOLYLOOP((#201,#202,#206,#205)); + #134=IFCPOLYLOOP((#202,#203,#207,#206)); + #135=IFCPOLYLOOP((#203,#200,#204,#207)); + #200=IFCCARTESIANPOINT((0.,0.,0.)); + #201=IFCCARTESIANPOINT((4000.,0.,0.)); + #202=IFCCARTESIANPOINT((4000.,200.,0.)); + #203=IFCCARTESIANPOINT((0.,200.,0.)); + #204=IFCCARTESIANPOINT((0.,0.,3000.)); + #205=IFCCARTESIANPOINT((4000.,0.,3000.)); + #206=IFCCARTESIANPOINT((4000.,200.,3000.)); + #207=IFCCARTESIANPOINT((0.,200.,3000.)); + #300=IFCPROPERTYSINGLEVALUE('FireRating',$,IFCLABEL('EI 60'),$); + #301=IFCPROPERTYSET('2SmokePsetWall00000001',$,'Pset_WallCommon',$,(#300)); + #302=IFCRELDEFINESBYPROPERTIES('2SmokeDefinesWall00001',$,$,$,(#100),#301); + #400=IFCWINDOW('1SmokeWindow0000000001',$,'Window',$,$,#401,#402,$,1000.,1000.); + #401=IFCLOCALPLACEMENT(#41,#403); + #403=IFCAXIS2PLACEMENT3D(#404,$,$); + #404=IFCCARTESIANPOINT((0.,-1000.,0.)); + #402=IFCPRODUCTDEFINITIONSHAPE($,$,(#405)); + #405=IFCSHAPEREPRESENTATION(#20,'Body','Brep',(#406)); + #406=IFCFACETEDBREP(#105); + #500=IFCBUILDINGELEMENTPROXY('1SmokeHostile000000001',$,'{Hostile}',$,$,#501,#502,$,$); + #501=IFCLOCALPLACEMENT(#41,#503); + #503=IFCAXIS2PLACEMENT3D(#504,$,$); + #504=IFCCARTESIANPOINT((0.,1000.,0.)); + #502=IFCPRODUCTDEFINITIONSHAPE($,$,(#505)); + #505=IFCSHAPEREPRESENTATION(#20,'Body','Brep',(#506)); + #506=IFCFACETEDBREP(#105); + #510=IFCPROPERTYSINGLEVALUE('{Hostile}',$,IFCLABEL('{Hostile}'),$); + #511=IFCPROPERTYSET('2SmokePsetHostile00001',$,'{Hostile}',$,(#510)); + #512=IFCRELDEFINESBYPROPERTIES('2SmokeDefinesHostile01',$,$,$,(#500),#511); + #600=IFCRELCONTAINEDINSPATIALSTRUCTURE('2SmokeContainment00001',$,$,$,(#100,#400,#500),#40); + ENDSEC; + END-ISO-10303-21; + """; + + [Fact] + public void Cli_ConvertIfcToHtml_ReadsTheModelThroughIfcIO() + { + var tempIfc = Path.Combine(Path.GetTempPath(), $"test_model_{Guid.NewGuid():N}.ifc"); + var tempHtml = Path.ChangeExtension(tempIfc, ".html"); + var tempJson = Path.ChangeExtension(tempIfc, ".json"); + using var errors = new StringWriter(); + var origErr = Console.Error; + try + { + File.WriteAllText(tempIfc, SmokeIfc); + Console.SetError(errors); + + int code = Program.Main([tempIfc, "--json", tempJson]); + + Assert.Equal(0, code); + Assert.Equal("", errors.ToString()); + var scene = JsonViewerImporter.Load(tempJson).Scene; + Assert.Empty(scene.Validate()); + Assert.Equal(Path.GetFileName(tempIfc), Assert.Single(scene.Models).Key); + + var view = ViewerHtml.Parse(File.ReadAllText(tempHtml)); + Assert.Equal("Smoke", view.Meta.GetProperty("title").GetString()); + Assert.Equal(3, view.InstanceCount); + var elements = view.Meta.GetProperty("elements").EnumerateArray().ToList(); + + // The wall's property set and GlobalId reach the page. + Assert.Equal("EI 60", view.Props.GetProperty("100").GetProperty("Pset_WallCommon").GetProperty("FireRating").GetString()); + var wall = Assert.Single(elements, e => e.GetProperty("id").GetInt64() == 100); + Assert.Equal("1SmokeWall000000000001", wall.GetProperty("sourceId").GetString()); + + // Hostile names and values are kept as data and escaped in the page. + Assert.Equal(Hostile, view.Props.GetProperty("500").GetProperty(Hostile).GetProperty(Hostile).GetString()); + Assert.Equal(Hostile, Assert.Single(elements, e => e.GetProperty("id").GetInt64() == 500).GetProperty("name").GetString()); + Assert.DoesNotContain(Hostile, view.Html, StringComparison.Ordinal); + + // The window has no style, so it is drawn in its class colour at half opacity (alpha 128 of 255). + Assert.Contains( + view.Meta.GetProperty("materials").EnumerateArray(), + m => Math.Abs(m.GetProperty("opacity").GetDouble() - 128 / 255.0) < 1e-6); + } + finally + { + Console.SetError(origErr); + foreach (var file in new[] { tempIfc, tempHtml, tempJson }) + { + if (File.Exists(file)) File.Delete(file); + } + } + } + + // A real Revit file cannot be committed; run with IOVIEWER_RVT_FILE set to one before a release. + [RvtFileFact] + public void Cli_ConvertRvtToHtml_ReadsTheModelThroughRvtIO() + { + var rvt = Environment.GetEnvironmentVariable(RvtFileFactAttribute.Variable)!; + var tempHtml = Path.Combine(Path.GetTempPath(), $"test_viewer_{Guid.NewGuid():N}.html"); + var tempJson = Path.ChangeExtension(tempHtml, ".json"); + try + { + int code = Program.Main([rvt, "-o", tempHtml, "--json", tempJson]); + + Assert.Equal(0, code); + Assert.Empty(JsonViewerImporter.Load(tempJson).Scene.Validate()); + Assert.True(ViewerHtml.Parse(File.ReadAllText(tempHtml)).InstanceCount > 0); + } + finally + { + if (File.Exists(tempHtml)) File.Delete(tempHtml); + if (File.Exists(tempJson)) File.Delete(tempJson); + } + } + + private sealed class RvtFileFactAttribute : FactAttribute + { + public const string Variable = "IOVIEWER_RVT_FILE"; + + public RvtFileFactAttribute() + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Variable))) + { + Skip = $"Set {Variable} to the path of a Revit 2022 file to run this test."; + } + } + } } From 36152b04c27e47d1bba1ecdabdedce378dad26e0 Mon Sep 17 00:00:00 2001 From: mahach666 <79989386+mahach666@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:24:10 +0300 Subject: [PATCH 2/5] List the engines the CLI tool package ships in the notices --- THIRD-PARTY-NOTICES.md | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 8106a2c..28bfa91 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -35,3 +35,47 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + +## IOViewer.Cli tool package + +The `IOViewer.Cli` tool package reads IFC and Revit files through the engine adapters, so besides `IOViewer` and +`IOScene` (MIT, copyright (c) 2026 mahach666) it ships the assemblies listed below. The `IOViewer` library package +references none of them. + +| Component | Use | License | +|---|---|---| +| [IfcIO](https://github.com/IOExtend/IfcIO) 0.11.0 and IfcIO.Scene 0.11.0 | read IFC files into a scene | MIT, copyright (c) 2026 IfcIO contributors | +| [RvtIO](https://github.com/IOExtend/RvtIO) 0.4.0 and RvtIO.Scene 0.4.0 | read Revit files into a scene | MIT, copyright (c) 2026 mahach666 | +| .NET runtime libraries (DeflateManaged sources, compiled into RvtIO) | managed inflate implementation adapted for tolerant decoding | MIT, copyright (c) .NET Foundation and Contributors | +| [OpenMcdf](https://github.com/ironfede/openmcdf) 3.4.0, used by RvtIO | reads the compound file container of Revit files | MPL-2.0, copyright (c) 2010-2026 Federico Blaseotto, Jeremy Powell | +| [SharpZipLib](https://github.com/icsharpcode/SharpZipLib) 1.4.2, used by RvtIO | stream decompression | MIT, copyright (c) 2000-2022 SharpZipLib Contributors | + +OpenMcdf is distributed unmodified under the Mozilla Public License 2.0 (); its source +code is available at . + +The MIT-licensed components above are distributed under the following terms, each with the copyright line given in the +table: + +```text +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +buildingSMART and IFC are registered trademarks or standards of buildingSMART International Ltd.; IfcIO is an +independent implementation of the IFC schemas. Autodesk and Revit are trademarks of Autodesk, Inc.; RvtIO is not +affiliated with, endorsed by or sponsored by Autodesk. From 7c2c72a7dd036e21bf99758e69f1bdeba248f477 Mon Sep 17 00:00:00 2001 From: mahach666 <79989386+mahach666@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:24:10 +0300 Subject: [PATCH 3/5] Document IFC and RVT input of the CLI --- CHANGELOG.md | 5 +++++ CONTRIBUTING.md | 1 + README.md | 9 +++++++-- docs/getting-started.md | 7 ++++++- docs/index.md | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de01d9..2739550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,11 @@ the `IOViewer` package). The `IOViewer.Cli` tool only gains inputs. - The HTML viewer shows `SceneElement.SourceId`, the element's id in its source format (such as a Revit UniqueId or an IFC GlobalId), as its own row in the properties panel ("Source ID" / "ID источника"), and the model tree search finds it. It is written to the page's element list only when set. +- The `ioviewer` tool reads IFC (`.ifc`, `.ifczip`) through `IfcIO.Scene` 0.11.0 and Revit (`.rvt`) through + `RvtIO.Scene` 0.4.0. The model key is the file name; the title is the IFC project name, or the Revit file name. What + the engine could not convert (such as an element without geometry) is printed to standard error as one warning per + kind with its count. The `IOViewer` library still references no engine; only the `IOViewer.Cli` tool package ships + IfcIO, RvtIO and their dependencies (see `THIRD-PARTY-NOTICES.md`). ### Changed - `JsonViewerExporter` writes viewer JSON format 2: `{ "Kind": "ioviewer", "Format": 2, "Scene", "SectionPlanes", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7582c7..a242148 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,6 +132,7 @@ The workflow builds and tests the solution, publishes the NuGet packages through packages, symbols, source and checksums to a GitHub release, marked as a prerelease when the version has a hyphen (`v0.3.0-preview.1`). The release notes are the section of `CHANGELOG.md` for the version, so add that section before the tag; the workflow stops before publishing when it is missing. +Before the tag, run the Revit test with a real Revit 2022 file, which CI does not have: set `IOVIEWER_RVT_FILE` to its path and run `dotnet test IOViewer.slnx -c Release --filter Cli_ConvertRvtToHtml`. The NuGet policy must name this repository, `release.yml`, the configured NuGet profile and the `IOViewer*` package pattern. ## Licensing contributions diff --git a/README.md b/README.md index bc7883d..0986cb1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![Platform](https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-512BD4?style=flat-square&logo=dotnet)](https://dotnet.microsoft.com/) -IOViewer is a fast, 100% managed .NET library and command line tool that generates standalone interactive 3D WebGL / HTML BIM & CAD model viewers from code, glTF/GLB, Wavefront OBJ, or JSON. Geometry read by sibling libraries ([RvtIO](https://github.com/IOExtend/RvtIO), [IfcIO](https://github.com/IOExtend/IfcIO), and [NwcIO](https://github.com/IOExtend/NwcIO)) can be passed in by building a `SceneDocument` in code. The scene model comes from the [IOScene](https://github.com/IOExtend/IOScene) package; IOViewer has no direct dependency on the format libraries. +IOViewer is a fast, 100% managed .NET library and command line tool that generates standalone interactive 3D WebGL / HTML BIM & CAD model viewers from code, glTF/GLB, Wavefront OBJ, or JSON. Geometry read by sibling libraries ([RvtIO](https://github.com/IOExtend/RvtIO), [IfcIO](https://github.com/IOExtend/IfcIO), and [NwcIO](https://github.com/IOExtend/NwcIO)) can be passed in by building a `SceneDocument` in code, and the `ioviewer` command line tool reads IFC and Revit files directly through IfcIO and RvtIO. The scene model comes from the [IOScene](https://github.com/IOExtend/IOScene) package; the IOViewer library has no dependency on the format libraries. The generated HTML is a single file with no external dependencies that works offline: it opens from disk (`file://`) in a current browser with WebGL 2 and makes no network requests. The viewer is built on [three.js](https://threejs.org/), which is embedded in the file (see [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)). @@ -80,6 +80,10 @@ ioviewer model.glb -o viewer.html # Convert Wavefront OBJ model to interactive HTML viewer ioviewer model.obj -o viewer.html +# Convert an IFC model (.ifc or .ifczip, read with IfcIO) or a Revit model (.rvt, read with RvtIO) +ioviewer model.ifc +ioviewer model.rvt -o viewer.html + # Convert and immediately preview in default web browser via built-in HTTP server ioviewer model.glb --serve ``` @@ -91,7 +95,8 @@ Export structured JSON snapshots: ioviewer model.glb -o model.json ``` -Diagnostics and progress go to standard error. Run `ioviewer --help` for the full list of options. +Diagnostics and progress go to standard error, including what IfcIO or RvtIO could not convert (one warning per kind, +with its count). Run `ioviewer --help` for the full list of options. ## Library diff --git a/docs/getting-started.md b/docs/getting-started.md index 4ab6ba5..6ed2b6c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -16,12 +16,17 @@ dotnet add package IOViewer ## Command line usage -Convert any glTF 2.0 Binary (`.glb`) or Wavefront OBJ (`.obj`) into a self-contained 3D HTML viewer: +Convert any glTF 2.0 Binary (`.glb`), Wavefront OBJ (`.obj`), IFC (`.ifc`, `.ifczip`, read with IfcIO) or Revit +(`.rvt`, read with RvtIO) model into a self-contained 3D HTML viewer: ```sh # Convert 3D model into an interactive standalone HTML viewer ioviewer model.glb -o viewer.html +# IFC and Revit models; what the engine could not convert is printed as warnings on standard error +ioviewer model.ifc +ioviewer model.rvt + # Launch local preview server and open in default browser ioviewer model.glb --serve ``` diff --git a/docs/index.md b/docs/index.md index 2a616b0..042fb3c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ The generated HTML is a single file with no external dependencies that works off - [Getting started](getting-started.md): install the package or CLI tool and generate your first 3D viewer. - [Public API](public-api.md): supported surface and key entry points of the library. -- [Third-party notices](https://github.com/IOExtend/IOViewer/blob/main/THIRD-PARTY-NOTICES.md): the three.js license. +- [Third-party notices](https://github.com/IOExtend/IOViewer/blob/main/THIRD-PARTY-NOTICES.md): the three.js license and the components the command line tool ships. - [Source on GitHub](https://github.com/IOExtend/IOViewer) and [DeepWiki](https://deepwiki.com/IOExtend/IOViewer) for an automatically generated overview of the repository. IOViewer is not affiliated with, endorsed by or sponsored by Autodesk. Autodesk, Revit, Navisworks, ViewCube and TimeLiner are trademarks of Autodesk, Inc. From 8a96ee61aa4b7618a1dd6c9f7c85da3d4382cab1 Mon Sep 17 00:00:00 2001 From: mahach666 <79989386+mahach666@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:32:11 +0300 Subject: [PATCH 4/5] Report adapter diagnostics of Error severity as errors The CLI printed every adapter diagnostic other than Info with the prefix "Warning:". Diagnostics are now grouped by severity and code, and a group of Error severity is printed with "Error:". --- CHANGELOG.md | 6 ++-- README.md | 4 +-- docs/getting-started.md | 2 +- src/IOViewer.Cli/Program.cs | 11 ++++--- .../ModelAndCliIntegrationTests.cs | 32 +++++++++++++++++++ 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2739550..6e4cb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,9 +73,9 @@ the `IOViewer` package). The `IOViewer.Cli` tool only gains inputs. it. It is written to the page's element list only when set. - The `ioviewer` tool reads IFC (`.ifc`, `.ifczip`) through `IfcIO.Scene` 0.11.0 and Revit (`.rvt`) through `RvtIO.Scene` 0.4.0. The model key is the file name; the title is the IFC project name, or the Revit file name. What - the engine could not convert (such as an element without geometry) is printed to standard error as one warning per - kind with its count. The `IOViewer` library still references no engine; only the `IOViewer.Cli` tool package ships - IfcIO, RvtIO and their dependencies (see `THIRD-PARTY-NOTICES.md`). + the engine could not convert (such as an element without geometry) is printed to standard error as one warning or + error per kind with its count. The `IOViewer` library still references no engine; only the `IOViewer.Cli` tool + package ships IfcIO, RvtIO and their dependencies (see `THIRD-PARTY-NOTICES.md`). ### Changed - `JsonViewerExporter` writes viewer JSON format 2: `{ "Kind": "ioviewer", "Format": 2, "Scene", "SectionPlanes", diff --git a/README.md b/README.md index 0986cb1..84a4c84 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ Export structured JSON snapshots: ioviewer model.glb -o model.json ``` -Diagnostics and progress go to standard error, including what IfcIO or RvtIO could not convert (one warning per kind, -with its count). Run `ioviewer --help` for the full list of options. +Diagnostics and progress go to standard error, including what IfcIO or RvtIO could not convert (one warning or error +per kind, with its count). Run `ioviewer --help` for the full list of options. ## Library diff --git a/docs/getting-started.md b/docs/getting-started.md index 6ed2b6c..a32e751 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -23,7 +23,7 @@ Convert any glTF 2.0 Binary (`.glb`), Wavefront OBJ (`.obj`), IFC (`.ifc`, `.ifc # Convert 3D model into an interactive standalone HTML viewer ioviewer model.glb -o viewer.html -# IFC and Revit models; what the engine could not convert is printed as warnings on standard error +# IFC and Revit models; what the engine could not convert is printed as warnings or errors on standard error ioviewer model.ifc ioviewer model.rvt diff --git a/src/IOViewer.Cli/Program.cs b/src/IOViewer.Cli/Program.cs index a399a41..7f5c8dc 100644 --- a/src/IOViewer.Cli/Program.cs +++ b/src/IOViewer.Cli/Program.cs @@ -21,7 +21,7 @@ from 3D models (glTF/GLB, Wavefront OBJ, JSON, IFC, Revit). The HTML file has no external dependencies and works offline. JSON input is viewer JSON, an IOScene scene file or an IOViewer 0.1/0.2 snapshot. IFC (.ifc, .ifczip) is read with IfcIO and Revit (.rvt) with RvtIO; what they could not convert - is reported as warnings. + is reported as warnings or errors. Arguments: Path to input 3D model (.glb, .obj, .json, .ifc, .ifczip or .rvt). @@ -252,14 +252,15 @@ private static ViewerDocument FromRvt(string path) } // The engine adapters key the model by its file name, as the other importers do; the title falls back to it too. - private static ViewerDocument FromScene(string path, SceneDocument scene, IReadOnlyList diagnostics) + internal static ViewerDocument FromScene(string path, SceneDocument scene, IReadOnlyList diagnostics) { if (string.IsNullOrEmpty(scene.Title)) scene.Title = Path.GetFileNameWithoutExtension(path); - // Information entries (such as an element that has no geometry by design) are not warnings; one line per kind. - foreach (var kind in diagnostics.Where(d => d.Severity != SceneSeverity.Info).GroupBy(d => d.Code)) + // Information entries (such as an element that has no geometry by design) are not reported; one line per kind. + foreach (var kind in diagnostics.Where(d => d.Severity != SceneSeverity.Info).GroupBy(d => (d.Severity, d.Code))) { int more = kind.Count() - 1; - Console.Error.WriteLine($"Warning: {kind.Key}: {kind.First().Message}{(more > 0 ? $" ({more} more)" : "")}"); + string label = kind.Key.Severity == SceneSeverity.Error ? "Error" : "Warning"; + Console.Error.WriteLine($"{label}: {kind.Key.Code}: {kind.First().Message}{(more > 0 ? $" ({more} more)" : "")}"); } return new ViewerDocument(scene); } diff --git a/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs b/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs index 99771de..aa6ff94 100644 --- a/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs +++ b/tests/IOViewer.Tests/ModelAndCliIntegrationTests.cs @@ -326,6 +326,38 @@ public void Cli_ConvertIfcToHtml_ReadsTheModelThroughIfcIO() } } + [Fact] + public void Cli_AdapterDiagnostics_AreReportedOncePerSeverityAndKind() + { + using var errors = new StringWriter(); + var origErr = Console.Error; + try + { + Console.SetError(errors); + + Program.FromScene("model.rvt", new SceneDocument(), + [ + new(SceneSeverity.Error, "read-cut", "Cut one."), + new(SceneSeverity.Error, "read-cut", "Cut two."), + new(SceneSeverity.Warning, "read-cut", "Cut warning."), + new(SceneSeverity.Warning, "no-geometry", "No geometry."), + new(SceneSeverity.Info, "skipped", "Skipped."), + ]); + + Assert.Equal( + string.Join(Environment.NewLine, + "Error: read-cut: Cut one. (1 more)", + "Warning: read-cut: Cut warning.", + "Warning: no-geometry: No geometry.", + ""), + errors.ToString()); + } + finally + { + Console.SetError(origErr); + } + } + // A real Revit file cannot be committed; run with IOVIEWER_RVT_FILE set to one before a release. [RvtFileFact] public void Cli_ConvertRvtToHtml_ReadsTheModelThroughRvtIO() From 20950d5cccc8b36359697bae531c05a9db016c49 Mon Sep 17 00:00:00 2001 From: mahach666 <79989386+mahach666@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:32:21 +0300 Subject: [PATCH 5/5] Link OpenMcdf to the repository its 3.4.0 package names The OpenMcdf 3.4.0 package names github.com/openmcdf/openmcdf as its repository; the notices pointed to the former location. --- THIRD-PARTY-NOTICES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 28bfa91..ba1e240 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -47,11 +47,11 @@ references none of them. | [IfcIO](https://github.com/IOExtend/IfcIO) 0.11.0 and IfcIO.Scene 0.11.0 | read IFC files into a scene | MIT, copyright (c) 2026 IfcIO contributors | | [RvtIO](https://github.com/IOExtend/RvtIO) 0.4.0 and RvtIO.Scene 0.4.0 | read Revit files into a scene | MIT, copyright (c) 2026 mahach666 | | .NET runtime libraries (DeflateManaged sources, compiled into RvtIO) | managed inflate implementation adapted for tolerant decoding | MIT, copyright (c) .NET Foundation and Contributors | -| [OpenMcdf](https://github.com/ironfede/openmcdf) 3.4.0, used by RvtIO | reads the compound file container of Revit files | MPL-2.0, copyright (c) 2010-2026 Federico Blaseotto, Jeremy Powell | +| [OpenMcdf](https://github.com/openmcdf/openmcdf) 3.4.0, used by RvtIO | reads the compound file container of Revit files | MPL-2.0, copyright (c) 2010-2026 Federico Blaseotto, Jeremy Powell | | [SharpZipLib](https://github.com/icsharpcode/SharpZipLib) 1.4.2, used by RvtIO | stream decompression | MIT, copyright (c) 2000-2022 SharpZipLib Contributors | OpenMcdf is distributed unmodified under the Mozilla Public License 2.0 (); its source -code is available at . +code is available at . The MIT-licensed components above are distributed under the following terms, each with the copyright line given in the table: