diff --git a/CadRevealComposer/CadRevealComposerRunner.cs b/CadRevealComposer/CadRevealComposerRunner.cs
index 829a1e00..7ca84d4c 100644
--- a/CadRevealComposer/CadRevealComposerRunner.cs
+++ b/CadRevealComposer/CadRevealComposerRunner.cs
@@ -7,6 +7,7 @@ namespace CadRevealComposer
using Operations;
using Primitives;
using Primitives.Reflection;
+ using RvmSharp;
using RvmSharp.BatchUtils;
using RvmSharp.Containers;
using RvmSharp.Primitives;
@@ -38,7 +39,7 @@ public static async Task Process(
Console.WriteLine($"\t{x.fileName} ({x.progress}/{x.total})");
});
var stringInternPool = new BenStringInternPool(new SharedInternPool());
- var rvmStore = Workload.ReadRvmData(workload, progressReport, stringInternPool);
+ var rvmStore = Workload.ReadRvmData(workload, progressReport, stringInternPool, new RvmReadOptions(true, true, true));
var fileSizesTotal = workload.Sum(w => new FileInfo(w.rvmFilename).Length);
Console.WriteLine(
$"Read RvmData in {rvmTimer.Elapsed}. (~{fileSizesTotal / 1024 / 1024}mb of .rvm files (excluding .txt file size))");
diff --git a/CadRevealComposer/Operations/ZoneSplitter.cs b/CadRevealComposer/Operations/ZoneSplitter.cs
index 6764b68e..d3f6f140 100644
--- a/CadRevealComposer/Operations/ZoneSplitter.cs
+++ b/CadRevealComposer/Operations/ZoneSplitter.cs
@@ -14,7 +14,7 @@ namespace CadRevealComposer.Operations;
///
/// Divides a model into zones for a better starting point for sector splitting.
-/// This is needed for models spread over a larger area like Melkøya and Tjeldbergodden.
+/// This is needed for models spread over a larger area like Melkoya and Tjeldbergodden.
///
public static class ZoneSplitter
{
diff --git a/RvmSharp.Exe/Options.cs b/RvmSharp.Exe/Options.cs
index 51ac7edc..b2d8d4aa 100644
--- a/RvmSharp.Exe/Options.cs
+++ b/RvmSharp.Exe/Options.cs
@@ -5,12 +5,13 @@
internal class Options
{
- public Options(IEnumerable inputs, string filter, string output, float tolerance)
+ public Options(IEnumerable inputs, string filter, string output, float tolerance, bool optimize)
{
Inputs = inputs;
Filter = filter;
Output = output;
Tolerance = tolerance;
+ Optimize = optimize;
}
[Option('i', "input", Required = true, HelpText = "Input file or folder, can specify multiple items")]
@@ -24,5 +25,9 @@ public Options(IEnumerable inputs, string filter, string output, float t
[Option('t', "tolerance", Default = 0.1f, Required = false, HelpText = "Tessellation tolerance")]
public float Tolerance { get; }
+
+ [Option('z', "optimize", Required = false,
+ HelpText = "Replace simple pyramids and snouts by boxes and cylinders")]
+ public bool Optimize { get; }
}
}
\ No newline at end of file
diff --git a/RvmSharp.Exe/Program.cs b/RvmSharp.Exe/Program.cs
index 2a9111fc..5451e6a9 100644
--- a/RvmSharp.Exe/Program.cs
+++ b/RvmSharp.Exe/Program.cs
@@ -34,7 +34,8 @@ private static int RunOptionsAndReturnExitCode(Options options)
using var parentProgressBar = new ProgressBar(2, "Converting RVM to OBJ");
- var rvmStore = ReadRvmData(workload);
+ var readOptions = options.Optimize ? new RvmReadOptions(true, true, true) : new RvmReadOptions();
+ var rvmStore = ReadRvmData(workload, readOptions);
using var connectProgressBar = parentProgressBar.Spawn(2, "Connecting geometry");
RvmConnect.Connect(rvmStore);
@@ -51,6 +52,7 @@ private static int RunOptionsAndReturnExitCode(Options options)
((i) => tessellationProgressBar.MaxTicks = i, () => tessellationProgressBar.Tick()),
((i) => exportProgressBar.MaxTicks = i, () => exportProgressBar.Tick()));
parentProgressBar.Tick();
+ parentProgressBar.Dispose(); // keep to be able to write to console
Console.WriteLine("Done!");
return 0;
}
@@ -95,7 +97,7 @@ into filePairStatic
return result.ToArray();
}
- private static RvmStore ReadRvmData(IReadOnlyCollection<(string rvmFilename, string? txtFilename)> workload)
+ private static RvmStore ReadRvmData(IReadOnlyCollection<(string rvmFilename, string? txtFilename)> workload, RvmReadOptions options)
{
using var progressBar = new ProgressBar(workload.Count, "Parsing input");
@@ -104,7 +106,7 @@ private static RvmStore ReadRvmData(IReadOnlyCollection<(string rvmFilename, str
(string rvmFilename, string? txtFilename) = filePair;
progressBar.Message = Path.GetFileNameWithoutExtension(rvmFilename);
using var stream = File.OpenRead(rvmFilename);
- var rvmFile = RvmParser.ReadRvm(stream);
+ var rvmFile = RvmParser.ReadRvm(stream, options);
if (!string.IsNullOrEmpty(txtFilename))
{
rvmFile.AttachAttributes(txtFilename);
diff --git a/RvmSharp/BatchUtils/Workload.cs b/RvmSharp/BatchUtils/Workload.cs
index 3b7968f6..202198c3 100644
--- a/RvmSharp/BatchUtils/Workload.cs
+++ b/RvmSharp/BatchUtils/Workload.cs
@@ -53,16 +53,19 @@ into filePairStatic
public static RvmStore ReadRvmData(
IReadOnlyCollection<(string rvmFilename, string? txtFilename)> workload,
IProgress<(string fileName, int progress, int total)>? progressReport = null,
- IStringInternPool? stringInternPool = null)
+ IStringInternPool? stringInternPool = null,
+ RvmReadOptions? rvmReadOptions = null)
{
var progress = 0;
var redundantPdmsAttributesToExclude = new[] { "Name", "Position" };
+ rvmReadOptions ??= new RvmReadOptions();
+
RvmFile ParseRvmFile((string rvmFilename, string? txtFilename) filePair)
{
(string rvmFilename, string? txtFilename) = filePair;
using var stream = File.OpenRead(rvmFilename);
- var rvmFile = RvmParser.ReadRvm(stream);
+ var rvmFile = RvmParser.ReadRvm(stream, rvmReadOptions);
if (!string.IsNullOrEmpty(txtFilename))
{
rvmFile.AttachAttributes(txtFilename!, redundantPdmsAttributesToExclude, stringInternPool);
diff --git a/RvmSharp/Operations/BoxDetector.cs b/RvmSharp/Operations/BoxDetector.cs
new file mode 100644
index 00000000..ad885740
--- /dev/null
+++ b/RvmSharp/Operations/BoxDetector.cs
@@ -0,0 +1,276 @@
+namespace RvmSharp.Operations;
+
+using Primitives;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+public static class BoxDetector
+{
+ public static bool IsBox(RvmFacetGroup facetGroup, out RvmBox box)
+ {
+ box = new RvmBox(1,
+ Matrix4x4.Identity,
+ new RvmBoundingBox(Vector3.Zero, Vector3.Zero),
+ 0, 0, 0);
+
+ if (facetGroup.Polygons.Length != 6)
+ return false;
+
+ var sides = new (Vector3[] vertices, Vector3 normal)[6];
+ // we need 6 sides
+
+ // collect sides and check normal on all vertices
+ for (var i = 0; i < facetGroup.Polygons.Length; i++)
+ {
+ var polygon = facetGroup.Polygons[i];
+ if (polygon.Contours.Length != 1)
+ return false;
+ foreach (var contour in polygon.Contours)
+ {
+ var vs = contour.Vertices;
+ if (vs.Length != 4)
+ return false;
+ if (vs[0].Normal != vs[1].Normal ||
+ vs[1].Normal != vs[2].Normal ||
+ vs[2].Normal != vs[3].Normal)
+ return false;
+
+ sides[i] = (new[] { vs[0].Vertex, vs[1].Vertex, vs[2].Vertex, vs[3].Vertex }, vs[0].Normal);
+ }
+ }
+
+ // Check that all angles in each side are 90 degrees
+ foreach (var side in sides)
+ {
+ var a1 = AngleToRad(side.vertices[1] - side.vertices[0], side.vertices[3] - side.vertices[0]);
+ var a2 = AngleToRad(side.vertices[1] - side.vertices[2], side.vertices[2] - side.vertices[3]);
+ var a3 = AngleToRad(side.vertices[0] - side.vertices[1], side.vertices[2] - side.vertices[1]);
+ if (!ApproximatelyEquals(a1, MathF.PI / 2) || !ApproximatelyEquals(a2, MathF.PI / 2) || !ApproximatelyEquals(a3, MathF.PI / 2))
+ return false;
+ }
+
+ // Check that we have 8 unique vertices
+ var uniqueVertices = new List(8);
+ foreach (var side in sides)
+ {
+ foreach (var v in side.vertices)
+ {
+ if (uniqueVertices.Any(uv => uv.Equals(v)))
+ continue;
+ if (uniqueVertices.Count == 8) // we have more than 8 unique vertices
+ return false;
+ uniqueVertices.Add(v);
+ }
+ }
+ if (uniqueVertices.Count != 8)
+ return false;
+
+ // Take 3 unique vertices, construct box verify that every side exists
+
+ // Find vertex not on the first side
+ Vector3 origin = default;
+ Vector3 dir1 = default;
+ Vector3 dir2 = default;
+ Vector3 dir3 = default;
+ foreach (var side1VertexX in sides[1].vertices)
+ {
+ var side0Vertices = sides[0].vertices;
+ if (side0Vertices.Any(v => v.ApproximatelyEquals(side1VertexX)))
+ continue;
+
+ // find side 0 vertex that is connected to side 1 vertex X, by measuring angle
+ if (AngleToRad(side1VertexX - side0Vertices[0], side0Vertices[1] - side0Vertices[0]).ApproximatelyEquals(MathF.PI / 2))
+ {
+ // connected to side 0 vertex 0
+ origin = side0Vertices[0];
+ dir1 = side0Vertices[1] - origin;
+ dir2 = side0Vertices[3] - origin;
+ dir3 = side1VertexX - origin;
+ } else if (AngleToRad(side1VertexX - side0Vertices[1], side0Vertices[2] - side0Vertices[1]).ApproximatelyEquals(MathF.PI / 2))
+ {
+ // connected to side 0 vertex 1
+ origin = side0Vertices[1];
+ dir1 = side0Vertices[2] - origin;
+ dir2 = side0Vertices[0] - origin;
+ dir3 = side1VertexX - origin;
+ } else if (AngleToRad(side1VertexX - side0Vertices[2], side0Vertices[3] - side0Vertices[2]).ApproximatelyEquals(MathF.PI / 2))
+ {
+ // connected to side 0 vertex 2
+ origin = side0Vertices[2];
+ dir1 = side0Vertices[3] - origin;
+ dir2 = side0Vertices[1] - origin;
+ dir3 = side1VertexX - origin;
+ } else if (AngleToRad(side1VertexX - side0Vertices[3], side0Vertices[0] - side0Vertices[3]).ApproximatelyEquals(MathF.PI / 2))
+ {
+ // connected to side 0 vertex 3
+ origin = side0Vertices[3];
+ dir1 = side0Vertices[0] - origin;
+ dir2 = side0Vertices[2] - origin;
+ dir3 = side1VertexX - origin;
+ }
+ else
+ {
+ // something is not right, bail
+ return false;
+ }
+ break;
+ }
+
+ // Creating new box, from initial vertex and 3 directional vectors with lengths
+ var v1 = origin;
+ var v2 = origin + dir1;
+ var v3 = origin + dir1 + dir2;
+ var v4 = origin + dir2;
+ var v5 = origin + dir3;
+ var v6 = origin + dir3 + dir1;
+ var v7 = origin + dir3 + dir1 + dir2;
+ var v8 = origin + dir3 + dir2;
+ var n1 = Vector3.Normalize(dir1);
+ var n2 = Vector3.Normalize(dir2);
+ var n3 = Vector3.Normalize(dir3);
+ var newSides = new (Vector3[] vertices, Vector3 normal)[]
+ {
+ (new []{v1, v2, v3, v4}, -n3),
+ (new []{v5, v6, v7, v8}, n3),
+ (new []{v1, v2, v6, v5}, -n2),
+ (new []{v2, v3, v7, v6}, n1),
+ (new []{v3, v4, v8, v7}, n2),
+ (new []{v4, v1, v5, v8}, -n1),
+ };
+
+ // verify that all sides are present and found in original facet group
+ foreach (var newSide in newSides)
+ {
+ var found = false;
+ foreach (var oldSide in sides)
+ {
+ if (!newSide.normal.ApproximatelyEquals(oldSide.normal))
+ continue;
+
+ foreach (var v in newSide.vertices)
+ {
+ if (!oldSide.vertices.Any(vv => vv.ApproximatelyEquals(v)))
+ return false;
+ }
+ found = true;
+ break;
+ }
+ if (!found)
+ return false;
+ }
+
+ if (!Matrix4x4.Decompose(facetGroup.Matrix, out var scale, out var rotation, out var translation))
+ return false; // no point in trying
+
+ var boxCenter = origin + (dir1 + dir2 + dir3) / 2;
+ // check that two directions are axis aligned, two is enough to determine that all 3 are aligned
+ var dir1NormalizedAbs = Vector3.Abs(Vector3.Normalize(dir1));
+ var dir2NormalizedAbs = Vector3.Abs(Vector3.Normalize(dir2));
+ if (dir1NormalizedAbs.X.ApproximatelyEquals(1) && dir2NormalizedAbs.Y.ApproximatelyEquals(1) ||
+ dir1NormalizedAbs.X.ApproximatelyEquals(1) && dir2NormalizedAbs.Z.ApproximatelyEquals(1) ||
+ dir1NormalizedAbs.Y.ApproximatelyEquals(1) && dir2NormalizedAbs.X.ApproximatelyEquals(1) ||
+ dir1NormalizedAbs.Y.ApproximatelyEquals(1) && dir2NormalizedAbs.Z.ApproximatelyEquals(1) ||
+ dir1NormalizedAbs.Z.ApproximatelyEquals(1) && dir2NormalizedAbs.X.ApproximatelyEquals(1) ||
+ dir1NormalizedAbs.Z.ApproximatelyEquals(1) && dir2NormalizedAbs.Y.ApproximatelyEquals(1))
+ {
+ // axis aligned box, lets choose optimal with identity transform
+ var size = Vector3.Abs(dir1 + dir2 + dir3);
+
+ box = box with
+ {
+ BoundingBoxLocal = new RvmBoundingBox(-size/2, size/2),
+ Matrix = Matrix4x4.CreateScale(scale) *
+ Matrix4x4.CreateFromQuaternion(rotation) *
+ Matrix4x4.CreateTranslation(boxCenter * scale) * Matrix4x4.CreateTranslation(translation),
+ LengthX = MathF.Abs(size.X),
+ LengthY = MathF.Abs(size.Y),
+ LengthZ = MathF.Abs(size.Z),
+ };
+ }
+ else
+ {
+ var rot1 = Vector3.UnitX.FromToRotation(Vector3.Normalize(dir1));
+ var yTransformed = Vector3.Normalize(Vector3.Transform(Vector3.UnitY, rot1));
+ var angle = yTransformed.AngleToRad(Vector3.Normalize(dir3));
+ var rotationNormal = Vector3.Cross(yTransformed, Vector3.Normalize(dir3));
+ var rot2 = rotationNormal.LengthSquared().ApproximatelyEquals(0) ?
+ Quaternion.Identity : Quaternion.CreateFromAxisAngle(Vector3.Normalize(rotationNormal), angle);
+
+ var lengthX = dir1.Length();
+ var lengthY = dir3.Length();
+ var lengthZ = dir2.Length();
+ var rotationCombined = Quaternion.Normalize(rot2 * rot1);
+
+ box = box with
+ {
+ BoundingBoxLocal = new RvmBoundingBox(new Vector3(-lengthX/2, - lengthY /2, -lengthZ /2),
+ new Vector3(lengthX/ 2, lengthY /2, lengthZ/2)),
+ Matrix = Matrix4x4.CreateScale(scale) *
+ Matrix4x4.CreateFromQuaternion(rotationCombined * rotation) *
+ // Box center already rotated, so no need to rotate
+ Matrix4x4.CreateTranslation(boxCenter * scale) * Matrix4x4.CreateTranslation(translation),
+ LengthX = lengthX,
+ LengthY = lengthY,
+ LengthZ = lengthZ,
+ };
+ }
+ return true;
+ }
+
+ private static bool ApproximatelyEquals(this Vector3 v1, Vector3 v2, float tolerance = 0.001f)
+ {
+ return ApproximatelyEquals(v1.X, v2.X, tolerance) &&
+ ApproximatelyEquals(v1.Y, v2.Y, tolerance) &&
+ ApproximatelyEquals(v1.Z, v2.Z, tolerance);
+ }
+
+ // This should be in helper class together with the rest of helper functions from CadRevealComposer
+ private static bool ApproximatelyEquals(this float f1, float f2, float tolerance = 0.001f)
+ {
+ return MathF.Abs(f1 - f2) < tolerance;
+ }
+
+ private static float AngleToRad(this Vector3 from, Vector3 to)
+ {
+ return MathF.Acos(Vector3.Dot(from, to) / (from.Length() * to.Length()));
+ }
+
+ public static Quaternion FromToRotation(this Vector3 from, Vector3 to)
+ {
+ var cross = Vector3.Cross(from, to);
+ if (cross.LengthSquared().ApproximatelyEquals(0f, 0.00000001f)) // true if vectors are parallel
+ {
+ var dot = Vector3.Dot(from, to);
+ if (dot < 0) // Vectors point in opposite directions
+ {
+ // We need to find an orthogonal to (to), non-zero vector (v)
+ // such as dot product of (v) and (to) is 0
+ // or satisfies following equation: to.x * v.x + to.y * v.y + to.z + v.z = 0
+ // below some variants depending on which components of (to) is 0
+ var xZero = to.X.ApproximatelyEquals(0);
+ var yZero = to.Y.ApproximatelyEquals(0);
+ var zZero = to.Z.ApproximatelyEquals(0);
+ Vector3 axes;
+ if (xZero && yZero)
+ axes = new Vector3(to.Z, 0, 0);
+ else if (xZero && zZero)
+ axes = new Vector3(to.Y, 0, 0);
+ else if (yZero && zZero)
+ axes = new Vector3(0, to.X, 0);
+ else if (xZero)
+ axes = new Vector3(0, -to.Z, -to.Y);
+ else if (yZero)
+ axes = new Vector3(-to.Z, 0, -to.X);
+ else
+ axes = new Vector3(-to.Y, -to.Z, 0);
+ return Quaternion.CreateFromAxisAngle(Vector3.Normalize(axes), MathF.PI * 2);
+ }
+ return Quaternion.Identity;
+ }
+
+ return Quaternion.CreateFromAxisAngle(Vector3.Normalize(cross), from.AngleToRad(to));
+ }
+
+}
\ No newline at end of file
diff --git a/RvmSharp/RvmParser.cs b/RvmSharp/RvmParser.cs
index 9fcf699a..227ed272 100644
--- a/RvmSharp/RvmParser.cs
+++ b/RvmSharp/RvmParser.cs
@@ -1,6 +1,7 @@
namespace RvmSharp
{
using Containers;
+ using Operations;
using Primitives;
using System;
using System.Collections.Generic;
@@ -86,7 +87,7 @@ private static (uint version, string project, string name) ReadModelParameters(S
return (version, project, name);
}
- private static RvmPrimitive ReadPrimitive(Stream stream)
+ private static RvmPrimitive ReadPrimitive(Stream stream, RvmReadOptions options)
{
var version = ReadUint(stream);
var kind = (RvmPrimitiveKind)ReadUint(stream);
@@ -109,8 +110,19 @@ private static RvmPrimitive ReadPrimitive(Stream stream)
var offsetX = ReadFloat(stream);
var offsetY = ReadFloat(stream);
var height = ReadFloat(stream);
- primitive = new RvmPyramid(version, matrix, bBoxLocal, bottomX, bottomY, topX, topY, offsetX,
- offsetY, height);
+
+ if (options.PyramidToBox &&
+ MathF.Abs(bottomX - topX) < options.OptimizationTolerance &&
+ MathF.Abs(bottomY - topY) < options.OptimizationTolerance &&
+ offsetX == 0 && offsetY == 0)
+ {
+ primitive = new RvmBox(version, matrix, bBoxLocal, bottomX, bottomY, height);
+ } else
+ {
+ primitive = new RvmPyramid(version, matrix, bBoxLocal, bottomX, bottomY, topX, topY,
+ offsetX,
+ offsetY, height);
+ }
break;
}
case RvmPrimitiveKind.Box:
@@ -163,8 +175,19 @@ private static RvmPrimitive ReadPrimitive(Stream stream)
var bottomShearY = ReadFloat(stream);
var topShearX = ReadFloat(stream);
var topShearY = ReadFloat(stream);
- primitive = new RvmSnout(version, matrix, bBoxLocal, radiusBottom, radiusTop, height,
- offsetX, offsetY, bottomShearX, bottomShearY, topShearX, topShearY);
+ if (options.SnoutToCylinder &&
+ Math.Abs(radiusBottom - radiusTop) < options.OptimizationTolerance &&
+ bottomShearX == 0 && bottomShearY == 0 &&
+ topShearX == 0 && topShearY == 0 &&
+ offsetX == 0 && offsetY == 0)
+ {
+ primitive = new RvmCylinder(version, matrix, bBoxLocal, radiusBottom, height);
+ }
+ else
+ {
+ primitive = new RvmSnout(version, matrix, bBoxLocal, radiusBottom, radiusTop, height,
+ offsetX, offsetY, bottomShearX, bottomShearY, topShearX, topShearY);
+ }
break;
}
case RvmPrimitiveKind.Cylinder:
@@ -211,11 +234,20 @@ private static RvmPrimitive ReadPrimitive(Stream stream)
// We order the polygons here so that we can have a better match rate when doing facet group matching.
// This simple change can improve facet matching depending on 3D model (~10% for Huldra), while the output is visually equal.
+
var polygonsOrdered = polygons
.OrderBy(p => p.Contours.Length) // OrderBy uses a stable sorting algorithm which preserves original ordering upon ordering equals
.ThenBy(p => p.Contours.Sum(c => c.Vertices.Length))
.ToArray();
- primitive = new RvmFacetGroup(version, matrix, bBoxLocal, polygonsOrdered);
+ var facetGroup = new RvmFacetGroup(version, matrix, bBoxLocal, polygonsOrdered);
+ if (options.FacetGroupToBox && BoxDetector.IsBox(facetGroup, out var box))
+ {
+ primitive = box;
+ }
+ else
+ {
+ primitive = facetGroup;
+ }
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unexpected Kind");
@@ -225,7 +257,7 @@ private static RvmPrimitive ReadPrimitive(Stream stream)
// transform bb to world?
}
- private static RvmNode ReadCntb(Stream stream)
+ private static RvmNode ReadCntb(Stream stream, RvmReadOptions options)
{
var version = ReadUint(stream);
var name = ReadString(stream);
@@ -254,10 +286,10 @@ private static RvmNode ReadCntb(Stream stream)
switch (id)
{
case "CNTB":
- group.AddChild(ReadCntb(stream));
+ group.AddChild(ReadCntb(stream, options));
break;
case "PRIM":
- group.AddChild(ReadPrimitive(stream));
+ group.AddChild(ReadPrimitive(stream, options));
break;
default:
throw new NotImplementedException($"Unknown chunk: {id}");
@@ -297,8 +329,9 @@ private static RvmFile.RvmHeader ReadHead(Stream stream)
return new RvmFile.RvmHeader(version, info, note, date, user, encoding);
}
- public static RvmFile ReadRvm(Stream stream)
+ public static RvmFile ReadRvm(Stream stream, RvmReadOptions? options = null)
{
+ options ??= new RvmReadOptions();
uint len, dunno;
var head = ReadChunkHeader(stream, out len, out dunno);
@@ -319,10 +352,10 @@ public static RvmFile ReadRvm(Stream stream)
switch (chunk)
{
case "CNTB":
- modelChildren.Add(ReadCntb(stream));
+ modelChildren.Add(ReadCntb(stream, options));
break;
case "PRIM":
- modelPrimitives.Add(ReadPrimitive(stream));
+ modelPrimitives.Add(ReadPrimitive(stream, options));
break;
case "COLR":
modelColors.Add(ReadColor(stream));
diff --git a/RvmSharp/RvmReadOptions.cs b/RvmSharp/RvmReadOptions.cs
new file mode 100644
index 00000000..6a1200bf
--- /dev/null
+++ b/RvmSharp/RvmReadOptions.cs
@@ -0,0 +1,8 @@
+namespace RvmSharp;
+
+public record RvmReadOptions(
+ bool PyramidToBox = false,
+ bool SnoutToCylinder = false,
+ bool FacetGroupToBox = false,
+ float OptimizationTolerance = 0.0001f
+ );
\ No newline at end of file