Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 2.4.0

### Fixed

- CSG `Union` produced corrupt geometry at large coordinates (e.g., survey-scale) due to BSP plane-side precision loss.
- LibTess tessellation threw `NullReferenceException` for geometry that produced contour synthesis points (T-junctions, self-intersections).
- Post-union polygon tessellation welded unrelated vertex corners at different positions when CSG `Shared.Tag` collided across faces.

## 2.1.0

### Added
Expand Down
54 changes: 54 additions & 0 deletions Elements.MEP/test/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Elements.Flow;
using Elements.Geometry;
using Elements.Geometry.Solids;
using Elements.Serialization.glTF;
using Xunit;

namespace Elements.MEP.Tests
Expand Down Expand Up @@ -37,6 +38,59 @@ public void TreeIsInitializedWhenDeserialized()
Assert.True(deserializedTree._alreadyTriedInit);
}

[Fact]
public void RoofDrain_UpdateRepresentations_UnionTessellatesToGlb()
{
var drain = CreateRoofDrainForUnionTest(0.55);
drain.UpdateRepresentations();

Assert.False(drain.Representation.SkipCSGUnion);
Assert.Equal(2, drain.Representation.SolidOperations.Count);

var model = new Model();
model.AddElement(drain);
var glb = model.ToGlTF(updateElementsRepresentations: false);
Assert.NotNull(glb);
Assert.NotEmpty(glb);
}

[Fact]
public void Tree_UpdateRepresentations_UnionTessellates()
{
var tree = FittingsTests.GetSampleTreeWithTrunkBelow();
tree.UpdateRepresentations();

Assert.False(tree.Representation.SkipCSGUnion);
Assert.NotEmpty(tree.Representation.SolidOperations);

tree.UpdateBoundsAndComputeSolid();
Assert.True(tree.Bounds.Max.Z - tree.Bounds.Min.Z > 0.01);

var model = new Model();
model.AddElement(tree);
var glb = model.ToGlTF(updateElementsRepresentations: false);
Assert.NotNull(glb);
Assert.NotEmpty(glb);
}

private static RoofDrain CreateRoofDrainForUnionTest(double diameter)
{
var drain = new RoofDrain(
diameter,
0,
0,
0,
false,
0,
Guid.NewGuid().ToString(),
id: Guid.NewGuid(),
name: "RD-test");
drain.ConnectorLength = 0.2764999948978424;
drain.ConnectorOuterDiameter = 0.075;
drain.HorizontalConnectorVector = new Vector3(0.5, 0, 0);
return drain;
}

[Fact]
public void RoofDrainTests()
{
Expand Down
Binary file modified Elements/lib/Csg.dll
Binary file not shown.
15 changes: 10 additions & 5 deletions Elements/src/Geometry/CsgExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private static void AddToMesh(this Csg.Polygon p, ref Mesh mesh)
};

tess.AddContour(p.Vertices.ToContourVertices());
tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3);
tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3, CombineCallbacks.CsgTexTagCombine);

for (var i = 0; i < tess.ElementCount; i++)
{
Expand All @@ -153,9 +153,9 @@ private static void AddToMesh(this Csg.Polygon p, ref Mesh mesh)
var b = t2.Position.ToVector3();
var c = t3.Position.ToVector3();

var dataA = ((Csg.Vector2D, int))t1.Data;
var dataB = ((Csg.Vector2D, int))t2.Data;
var dataC = ((Csg.Vector2D, int))t3.Data;
var dataA = TexTagOrDefault(t1.Data);
var dataB = TexTagOrDefault(t2.Data);
var dataC = TexTagOrDefault(t3.Data);

var v1 = mesh.FindOrCreateVertex(a, dataA.Item2, dataA.Item1.ToUV(), n);
var v2 = mesh.FindOrCreateVertex(b, dataB.Item2, dataB.Item1.ToUV(), n);
Expand All @@ -170,6 +170,11 @@ private static void AddToMesh(this Csg.Polygon p, ref Mesh mesh)
}
}

private static (Csg.Vector2D, int) TexTagOrDefault(object data)
{
return data is ValueTuple<Csg.Vector2D, int> t ? t : (new Csg.Vector2D(0, 0), 0);
}

private static Vector3 ToElementsVector(this Csg.Vector3D v)
{
return new Vector3(v.X, v.Y, v.Z);
Expand Down Expand Up @@ -244,7 +249,7 @@ internal static ContourVertex[] ToContourVertexArray(this IList<Csg.Vertex> vert
var cv = new ContourVertex
{
Position = new Vec3 { X = v.Pos.X, Y = v.Pos.Y, Z = v.Pos.Z },
Data = (v.Tex.ToUV(), (uint)v.Tag, faceId, solidId)
Data = new CsgVertexData(v.Tex.ToUV(), (uint)v.Tag, faceId, solidId)
};
contour[i] = cv;
}
Expand Down
12 changes: 6 additions & 6 deletions Elements/src/Geometry/Solids/Solid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -917,8 +917,8 @@ internal Csg.Solid ToCsg()
}
else
{
var vData1 = ((UV uv, uint tag, uint faceId, uint solidId))v1.Data;
av = csgVertices[(int)vData1.tag];
var vData1 = (CsgVertexData)v1.Data;
av = csgVertices[(int)vData1.Tag];
}

if (v2.Data == null)
Expand All @@ -928,8 +928,8 @@ internal Csg.Solid ToCsg()
}
else
{
var vData2 = ((UV uv, uint tag, uint faceId, uint solidId))v2.Data;
bv = csgVertices[(int)vData2.tag];
var vData2 = (CsgVertexData)v2.Data;
bv = csgVertices[(int)vData2.Tag];
}

if (v3.Data == null)
Expand All @@ -939,8 +939,8 @@ internal Csg.Solid ToCsg()
}
else
{
var vData3 = ((UV uv, uint tag, uint faceId, uint solidId))v3.Data;
cv = csgVertices[(int)vData3.tag];
var vData3 = (CsgVertexData)v3.Data;
cv = csgVertices[(int)vData3.Tag];
}

// Don't allow us to create a csg that has zero
Expand Down
2 changes: 1 addition & 1 deletion Elements/src/Geometry/Solids/SolidExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static ContourVertex[] ToContourVertexArray(this Loop loop, uint faceId
var cv = new ContourVertex
{
Position = new Vec3 { X = p.X, Y = p.Y, Z = p.Z },
Data = (default(UV), edge.Vertex.Id, faceId, solidId)
Data = new Tessellation.CsgVertexData(default, edge.Vertex.Id, faceId, solidId)
};
contour[i] = cv;
}
Expand Down
88 changes: 88 additions & 0 deletions Elements/src/Geometry/Tessellation/CombineCallbacks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Threading;
using LibTessDotNet.Double;

namespace Elements.Geometry.Tessellation
{
/// <summary>
/// Shared <see cref="CombineCallback"/> factories for tessellators.
///
/// LibTess can synthesize new vertices at contour intersection / T-junction points
/// during <see cref="Tess.Tessellate(WindingRule, ElementType, int, CombineCallback)"/>.
/// Without a callback, those synthetic vertices end up with <c>Data == null</c>, which
/// later trips a <see cref="System.NullReferenceException"/> in
/// <see cref="Tessellation.PackTessellationsIntoBuffers"/> when it unboxes
/// <c>v.Data</c> to the expected shape.
///
/// Each callback interpolates the UV from the input vertices, preserves the
/// faceId/solidId of the first non-null input (these are constant within a single
/// face's tessellation), and assigns a unique synthetic tag drawn from a process-global
/// monotonically increasing counter starting in the upper half of the uint range so it
/// can never collide with the small, sequential tags emitted by the CSG library.
/// </summary>
internal static class CombineCallbacks
{
// Start synthetic tags in the upper half of the uint range so they cannot
// collide with Csg.Vertex.Tag values, which are sequentially allocated from 0.
private static long _dataCombineCounter = 0x80000000L;
private static long _csgTexTagCombineCounter = 0x90000000L;

/// <summary>
/// Combine callback for tessellation paths that attach
/// <see cref="CsgVertexData"/> to <see cref="ContourVertex.Data"/>.
/// </summary>
internal static CombineCallback DataCombine { get; } = CombineData;

/// <summary>
/// Combine callback for legacy mesh tessellation paths that store
/// <c>(Csg.Vector2D tex, int tag)</c> on <see cref="ContourVertex.Data"/>.
/// </summary>
internal static CombineCallback CsgTexTagCombine { get; } = CombineCsgTexTag;

private static object CombineData(Vec3 position, object[] data, double[] weights)
{
var uvU = 0.0;
var uvV = 0.0;
uint faceId = 0;
uint solidId = 0;
bool seenInput = false;

for (var i = 0; i < data.Length; i++)
{
if (data[i] is CsgVertexData t)
{
var w = weights[i];
uvU += t.Uv.U * w;
uvV += t.Uv.V * w;
if (!seenInput)
{
faceId = t.FaceId;
solidId = t.SolidId;
seenInput = true;
}
}
}

var tag = (uint)Interlocked.Increment(ref _dataCombineCounter);
return new CsgVertexData(new UV(uvU, uvV), tag, faceId, solidId);
}

private static object CombineCsgTexTag(Vec3 position, object[] data, double[] weights)
{
var texX = 0.0;
var texY = 0.0;
for (var i = 0; i < data.Length; i++)
{
if (data[i] is ValueTuple<Csg.Vector2D, int> t)
{
var w = weights[i];
texX += t.Item1.X * w;
texY += t.Item1.Y * w;
}
}

var tag = (int)Interlocked.Increment(ref _csgTexTagCombineCounter);
return (new Csg.Vector2D(texX, texY), tag);
}
}
}
6 changes: 5 additions & 1 deletion Elements/src/Geometry/Tessellation/CsgPolygonTessAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public Tess GetTess()
};
tess.AddContour(polygon.Vertices.ToContourVertexArray(faceId, solidId));

tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3);
// Register a combine callback so vertices synthesized by LibTess at
// intersection / T-junction points carry the same CsgVertexData shape as
// the input vertices. Without this, the synthesized vertices' Data field
// is null and downstream packing throws a NullReferenceException.
tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3, CombineCallbacks.DataCombine);
return tess;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ public IEnumerable<ITessAdapter> GetTessellationTargets()
{
foreach (var p in csg.Polygons)
{
// We used the polygon's shared tag, which seems to
// work for planar solids turned into csgs as a discriminator,
// but this may break in the future.
// Shared.Tag groups coplanar polygons so the pack shares vertices across
// them; post-union collisions are caught by position-matched reuse downstream.
yield return new CsgPolygonTessAdapter(p, (uint)p.Shared.Tag, solidId);
}
}
Expand Down
23 changes: 23 additions & 0 deletions Elements/src/Geometry/Tessellation/CsgVertexData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Elements.Geometry.Tessellation
{
/// <summary>
/// Per-vertex data attached to <see cref="LibTessDotNet.Double.ContourVertex.Data"/>
/// by the CSG and solid-face tessellation adapters, and consumed by
/// <see cref="Tessellation.PackTessellationsIntoBuffers"/>.
/// </summary>
internal readonly struct CsgVertexData
{
public readonly UV Uv;
public readonly uint Tag;
public readonly uint FaceId;
public readonly uint SolidId;

public CsgVertexData(UV uv, uint tag, uint faceId, uint solidId)
{
Uv = uv;
Tag = tag;
FaceId = faceId;
SolidId = solidId;
}
}
}
6 changes: 5 additions & 1 deletion Elements/src/Geometry/Tessellation/SolidFaceTessAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ public Tess GetTess()
}
}

tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3);
// Register a combine callback so vertices synthesized by LibTess at
// intersection / T-junction points carry the same CsgVertexData shape as
// the input vertices. Without this, the synthesized vertices' Data field
// is null and downstream packing throws a NullReferenceException.
tess.Tessellate(WindingRule.Positive, ElementType.Polygons, 3, CombineCallbacks.DataCombine);
return tess;
}
}
Expand Down
Loading
Loading