Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,95 +1,188 @@
using System.Diagnostics.CodeAnalysis;
using Autodesk.Revit.DB;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;

namespace Speckle.Converters.RevitShared.ToHost.TopLevel;

public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<DB.GeometryObject>>
public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<GeometryObject>>
{
private readonly RevitToHostCacheSingleton _revitToHostCacheSingleton;
private readonly ScalingServiceToHost _scalingServiceToHost;
private readonly IReferencePointConverter _referencePointConverter;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private const double PLANAR_TOLERANCE = 1e-9; // tune if needed, added to avoid numeric noise
private const bool ALLOW_VERTEX_COLOR_OVERRIDE = true; // flip to true if colors should win

private Document? _lastDoc; // if this converter instance is used across open documents, we'll want to invalidate the material cache

public MeshConverterToHost(
RevitToHostCacheSingleton revitToHostCacheSingleton,
ScalingServiceToHost scalingServiceToHost,
IReferencePointConverter referencePointConverter
IReferencePointConverter referencePointConverter,
IConverterSettingsStore<RevitConversionSettings> converterSettings
)
{
_revitToHostCacheSingleton = revitToHostCacheSingleton;
_scalingServiceToHost = scalingServiceToHost;
_referencePointConverter = referencePointConverter;
_converterSettings = converterSettings;
}

public List<DB.GeometryObject> Convert(SOG.Mesh mesh)
public List<GeometryObject> Convert(SOG.Mesh mesh)
{
TessellatedShapeBuilderTarget target = TessellatedShapeBuilderTarget.Mesh;
TessellatedShapeBuilderFallback fallback = TessellatedShapeBuilderFallback.Salvage;
const TessellatedShapeBuilderTarget TARGET = TessellatedShapeBuilderTarget.Mesh;
const TessellatedShapeBuilderFallback FALLBACK = TessellatedShapeBuilderFallback.Salvage;

using var tsb = new TessellatedShapeBuilder()
{
Fallback = fallback,
Target = target,
GraphicsStyleId = ElementId.InvalidElementId
};
using var tsb = new TessellatedShapeBuilder();
tsb.Fallback = FALLBACK;
tsb.Target = TARGET;
tsb.GraphicsStyleId = ElementId.InvalidElementId;
// tsb.OpenConnectedFaceSet(false);

tsb.OpenConnectedFaceSet(false);
var vertices = ArrayToPoints(mesh.vertices, mesh.units);
var vertColors = DecodeVertexColors(mesh.colors);

ElementId materialId = ElementId.InvalidElementId;
// optional default material from cache
ElementId defaultMat = ElementId.InvalidElementId;
if (
_revitToHostCacheSingleton.MaterialsByObjectId.TryGetValue(
mesh.applicationId ?? mesh.id.NotNull(),
out var mappedElementId
out var mapped
)
)
{
materialId = mappedElementId;
defaultMat = mapped;
}

bool hasExplicitMat = defaultMat != ElementId.InvalidElementId;

var facesByMat = new Dictionary<ElementId, List<IList<XYZ>>>();

int i = 0;
while (i < mesh.faces.Count)
{
int n = mesh.faces[i];
if (n < 3)
int faceVertexCount = mesh.faces[i];
if (faceVertexCount < 3)
{
n += 3; // 0 -> 3, 1 -> 4 to preserve backwards compatibility
faceVertexCount += 3;
}

var points = mesh.faces.GetRange(i + 1, n).Select(x => vertices[x]).ToArray();
var faceIdx = mesh.faces.GetRange(i + 1, faceVertexCount);
var points = new XYZ[faceVertexCount];
for (int k = 0; k < faceVertexCount; k++)
{
points[k] = vertices[faceIdx[k]];
}

if (IsNonPlanarQuad(points))
var faceMaterial = FaceMat(faceIdx);
switch (faceVertexCount)
{
// Non-planar quads will be triangulated as it's more desirable than `TessellatedShapeBuilder.Build`'s attempt to make them planar.
// TODO consider triangulating all n > 3 polygons that are non-planar
var triPoints = new List<XYZ> { points[0], points[1], points[3] };
var face1 = new TessellatedFace(triPoints, materialId);
tsb.AddFace(face1);
case 4 when IsNonPlanarQuad(points):
{
// Non-planar quads will be triangulated as it's more desirable than
// TessellatedShapeBuilder.Build's attempt to make them planar.
AddFace([points[0], points[1], points[3]], faceMaterial);
AddFace([points[1], points[2], points[3]], faceMaterial);
break;
}
case > 4 when !IsPlanarNgon(points):
{
for (int k = 1; k < faceVertexCount - 1; k++)
{
AddFace([points[0], points[k], points[k + 1]], faceMaterial);
}
break;
}
default:
{
AddFace(points, faceMaterial);
break;
}
}

i += faceVertexCount + 1;
}

triPoints = new List<XYZ> { points[1], points[2], points[3] };
var all = new List<GeometryObject>();

foreach (var kv in facesByMat)
{
using var perMat = new TessellatedShapeBuilder();
perMat.Fallback = FALLBACK;
perMat.Target = TARGET;
perMat.GraphicsStyleId = ElementId.InvalidElementId;

var face2 = new TessellatedFace(triPoints, materialId);
tsb.AddFace(face2);
perMat.OpenConnectedFaceSet(true);
foreach (var tf in kv.Value.Select(pts => new TessellatedFace(pts, kv.Key)).Where(tf => tf.IsValidObject))
{
perMat.AddFace(tf);
}
else

perMat.CloseConnectedFaceSet();
perMat.Build();

all.AddRange(perMat.GetBuildResult().GetGeometricalObjects());
}

return all;

void AddFace(IList<XYZ> pts, ElementId mat)
{
if (!facesByMat.TryGetValue(mat, out var list))
{
var face = new TessellatedFace(points, materialId);
tsb.AddFace(face);
facesByMat[mat] = list = [];
}

i += n + 1;
list.Add(pts);
}

tsb.CloseConnectedFaceSet();
// local helper to pick a face material from vertex colors
[SuppressMessage("ReSharper", "RedundantLogicalConditionalExpressionOperand")]
ElementId FaceMat(IList<int> idx)
{
int vCount = vertColors.Length;
var hasColors = vCount > 0;

if (!hasColors || hasExplicitMat && !ALLOW_VERTEX_COLOR_OVERRIDE)
{
return defaultMat;
}

int sr = 0,
sg = 0,
sb = 0,
c = 0;
foreach (var v in idx)
{
if ((uint)v >= (uint)vCount)
{
continue;
}

var vc = vertColors[v];
sr += vc.Red;
sg += vc.Green;
sb += vc.Blue;
c++;
}

tsb.Build();
var result = tsb.GetBuildResult();
if (c == 0)
{
return defaultMat;
}

return result.GetGeometricalObjects().ToList();
byte r = Quant((byte)(sr / c));
byte g = Quant((byte)(sg / c));
byte b = Quant((byte)(sb / c));
return GetOrCreateMaterial(_converterSettings.Current.Document, r, g, b);
}
}

private static bool IsNonPlanarQuad(IList<XYZ> points)
Expand Down Expand Up @@ -117,7 +210,59 @@ private static bool IsNonPlanarQuad(IList<XYZ> points)
1,
1
);
return matrix.GetDeterminant() != 0;

return Math.Abs(matrix.GetDeterminant()) > PLANAR_TOLERANCE;
}

private static bool IsPlanarNgon(IList<XYZ> vertices)
{
int n = vertices.Count;
if (n < 4)
{
return true; // 3 points always define a plane
}

// Newell’s method for robust best-fit plane =>
// https://www.realtimerendering.com/resources/GraphicsGems/gemsiii/newell.c
double normalX = 0,
normalY = 0,
normalZ = 0;
for (int i = 0, j = n - 1; i < n; j = i, i++)
{
var u = vertices[i];
var v = vertices[j];
normalX += (v.Y - u.Y) * (v.Z + u.Z);
normalY += (v.Z - u.Z) * (v.X + u.X);
normalZ += (v.X - u.X) * (v.Y + u.Y);
}

var length = Math.Sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ);
if (length < 1e-12)
{
return true; // degenerate polygon; treat as planar
}

normalX /= length;
normalY /= length;
normalZ /= length;

var pointOnPlane = vertices[0];
double normalisedPlane = -(normalX * pointOnPlane.X + normalY * pointOnPlane.Y + normalZ * pointOnPlane.Z);

// max signed distance of all vertices to plane
double maxSignedDistance = 0;
for (int i = 1; i < n; i++)
{
var p = vertices[i];
double distance = normalX * p.X + normalY * p.Y + normalZ * p.Z + normalisedPlane;
maxSignedDistance = Math.Max(maxSignedDistance, Math.Abs(distance));
if (maxSignedDistance > PLANAR_TOLERANCE)
{
return false;
}
}

return true;
}

private XYZ[] ArrayToPoints(IList<double> arr, string units)
Expand All @@ -128,7 +273,7 @@ private XYZ[] ArrayToPoints(IList<double> arr, string units)
}

XYZ[] points = new XYZ[arr.Count / 3];
var fTypeId = _scalingServiceToHost.UnitsToNative(units) ?? UnitTypeId.Meters;
var fTypeId = _scalingServiceToHost.UnitsToNative(units);

for (int i = 2, k = 0; i < arr.Count; i += 3)
{
Expand All @@ -146,4 +291,89 @@ private XYZ[] ArrayToPoints(IList<double> arr, string units)

return points;
}

private readonly Dictionary<int, ElementId> _matCache = new();

private static Color[] DecodeVertexColors(IList<int>? argb)
{
if (argb == null)
{
return [];
}

var outArr = new Color[argb.Count];
for (int i = 0; i < argb.Count; i++)
{
uint v = unchecked((uint)argb[i]); // Speckle stores ARGB in a signed int
byte r = (byte)((v >> 16) & 0xFF);
byte g = (byte)((v >> 8) & 0xFF);
byte b = (byte)(v & 0xFF);

outArr[i] = new Color(r, g, b);
}

return outArr;
}

private static byte Quant(byte v, int step = 17)
{
int q = (int)Math.Round(v / (double)step) * step;
return (byte)Math.Max(0, Math.Min(255, q));
}

private ElementId GetOrCreateMaterial(Document doc, byte r, byte g, byte b)
{
if (!ReferenceEquals(doc, _lastDoc)) // essentially a document change check hack
{
_matCache.Clear();
_lastDoc = doc;
}

int key = (r << 16) | (g << 8) | b;
if (_matCache.TryGetValue(key, out var id))
{
return id;
}

string name = $"Speckle_DS_{r}_{g}_{b}";

Material? existing;
using (var filteredElementCollector = new FilteredElementCollector(doc))
{
filteredElementCollector.OfClass(typeof(Material)); // add the filter on the same instance
existing = filteredElementCollector
.Cast<Material>() // enumerate inside the using
.FirstOrDefault(m => string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase));
}

if (existing != null)
{
return _matCache[key] = existing.Id;
}

ElementId mid;
if (doc.IsModifiable)
{
using var st = new SubTransaction(doc);
st.Start();
mid = CreateMaterialWithColor(doc, name, r, g, b);
st.Commit();
}
else
{
using var t = new Transaction(doc, "Create DS Material");
t.Start();
mid = CreateMaterialWithColor(doc, name, r, g, b);
t.Commit();
}

return _matCache[key] = mid;

static ElementId CreateMaterialWithColor(Document doc, string name, byte r, byte g, byte b)
{
var materialId = Material.Create(doc, name);
((Material)doc.GetElement(materialId)).Color = new Color(r, g, b);
return materialId;
}
}
}
Loading