Skip to content

Conversation

@nsmela
Copy link

@nsmela nsmela commented Jan 3, 2026

Found getting volume and/or area to be a bit confusing at first. Added these methods to help less experienced users.

Wasn't abel to build for some reason, but this was a simple copy-and-paste.

@rms80
Copy link
Contributor

rms80 commented Jan 3, 2026

this is quite old code you have copied from (the VolumeArea() function) and could be written much more concisely against DMesh3 directly...eg I think this is equivalent for the volume inner loop, maybe you could clean it up (and test that the output is unmodified)?:

                mesh.GetTriVertices(tid, out Triangle3d tri);
                Vector3d N = (tri.V1 - tri.V0).Cross(tri.V2 - tri.V0);
                mass_integral += N.x * (tri.V0.x + tri.V1.x + tri.V2.x);

(and if it only actually needs N.x...)

@nsmela
Copy link
Author

nsmela commented Jan 3, 2026

Thanks. I'll try this and test it. I was also thinking of upgrading it to lazy property on DMesh3, but backed off because I like the functional programming approach MeshMeasurements is using. Plus it felt more SOLID to modify MeshMEasurements than adding a complicated proeprty to DMesh3

@nsmela
Copy link
Author

nsmela commented Jan 3, 2026

Using this for a unit test. I don't see a test suite, so I didn't want to pollute the repo.

namespace g3.Tests;

[TestFixture]
public class MeshMeasurementExtensionsTests {
    private const double Tolerance = 1e-5;

    // Helper to create a unit cube (1x1x1) centered at origin
    private DMesh3 CreateUnitCube() {
        return new TrivialBox3Generator() {
            Box = new Box3d(Vector3d.Zero, new Vector3d(0.5, 0.5, 0.5))
        }.Generate().MakeDMesh();
    }

    // Helper to create a unit cube (X - Y - Z) centered at origin
    private DMesh3 CreateUnitCube(Vector3d size) {
        return new TrivialBox3Generator() {
            Box = new Box3d(Vector3d.Zero, size / 2)
        }.Generate().MakeDMesh();
    }

    // Helper to create a flat 1x1 quad (2 triangles)
    private DMesh3 CreateUnitQuad() {
        return new TrivialRectGenerator() {
            Width = 1.0f,
            Height = 1.0f,
        }.Generate().MakeDMesh();
    }

    [Test]
    public void VolumeArea_UnitCube_ReturnsCorrectValues() {
        var mesh = CreateUnitCube();

        // Act
        Vector2d result = MeshMeasurementExtensions.VolumeArea(mesh);

        // Assert
        // Unit cube 1x1x1 -> Volume = 1.0
        Assert.That(result.x, Is.EqualTo(1.0).Within(Tolerance), "Volume should be 1.0");
        // 6 faces * 1x1 area -> Area = 6.0
        Assert.That(result.y, Is.EqualTo(6.0).Within(Tolerance), "Area should be 6.0");
    }

    [Test]
    public void VolumeArea_IrregularUnitCube_ReturnsCorrectValues() {
        var size = new Vector3d(2.0, 3.0, 4.0); // 24.0 vol
        var mesh = CreateUnitCube(size);

        // Act
        Vector2d result = MeshMeasurementExtensions.VolumeArea(mesh);

        // Assert
        // Unit cube 1x1x1 -> Volume = 24.0
        Assert.That(result.x, Is.EqualTo(24.0).Within(Tolerance), "Volume should be 24.0");
        // 6 faces * 1x1 area -> Area = 52.0
        Assert.That(result.y, Is.EqualTo(52.0).Within(Tolerance), "Area should be 52.0");
    }

    [Test]
    public void Volume_UnitCube_ReturnsCorrectVolume() {
        var mesh = CreateUnitCube();

        // Act
        double volume = MeshMeasurementExtensions.Volume(mesh);

        // Assert
        Assert.That(volume, Is.EqualTo(1.0).Within(Tolerance));
    }

    [Test]
    public void Area_UnitCube_ReturnsCorrectArea() {
        var mesh = CreateUnitCube();

        // Act
        double area = MeshMeasurementExtensions.Area(mesh);

        // Assert
        Assert.That(area, Is.EqualTo(6.0).Within(Tolerance));
    }

    [Test]
    public void VolumeArea_OpenQuad_ReturnsCorrectArea_NonsenseVolume() {
        // A flat plane does not enclose a volume, but the divergence theorem 
        // will still compute a value based on the projection to the origin.
        // We mainly want to ensure it doesn't crash and calculates Area correctly.
        var mesh = CreateUnitQuad();

        // Act
        Vector2d result = MeshMeasurementExtensions.VolumeArea(mesh);

        // Assert
        // 1x1 Rect -> Area = 1.0
        Assert.That(result.y, Is.EqualTo(1.0).Within(Tolerance), "Area should be 1.0");

        // Note: We don't assert Volume is 0 here because the Volume function 
        // relies on the mesh being closed. If the quad is offset from origin, 
        // the 'nonsense' volume will be non-zero.
    }

    [Test]
    public void VolumeArea_SubsetTriangles_CalculatesPartialArea() {
        var mesh = CreateUnitCube();

        // Let's only measure the "Top" face of the cube.
        // In TrivialBox3Generator, usually the last 2 triangles or specific indices form a face.
        // Instead of guessing indices, we'll filter triangles where Normal is +Y (0,1,0)

        var topFaceTriangles = new List<int>();
        foreach (int tid in mesh.TriangleIndices()) {
            mesh.GetTriVertices(tid, out Triangle3d tri);
            Vector3d normal = (tri.V1 - tri.V0).Cross(tri.V2 - tri.V0).Normalized;
            if (normal.Dot(Vector3d.AxisY) > 0.9)
                topFaceTriangles.Add(tid);
        }

        // Act
        // Use the overload: VolumeArea(mesh, triIndices, vertexFunc)
        // Just pass the standard vertex lookup
        Vector2d result = MeshMeasurementExtensions.VolumeArea(
            mesh,
            topFaceTriangles,
            (vid) => mesh.GetVertex(vid)
        );

        // Assert
        // Top face of unit cube is 1x1 -> Area = 1.0
        Assert.That(result.y, Is.EqualTo(1.0).Within(Tolerance));
    }

    [Test]
    public void VolumeArea_SubsetWithTransformFunction_ReturnsScaledValues() {
        var mesh = CreateUnitCube(); // 1x1x1

        // We want to simulate scaling the mesh by 2.0 WITHOUT changing the mesh geometry,
        // solely by using the func<int, Vector3d> delegate.
        double scale = 2.0;

        // Act
        Vector2d result = MeshMeasurementExtensions.VolumeArea(
            mesh,
            mesh.TriangleIndices(),
            (vid) => mesh.GetVertex(vid) * scale
        );

        // Assert
        // New Volume = 2^3 = 8
        // New Area = 6 * (2^2) = 24
        Assert.That(result.x, Is.EqualTo(8.0).Within(Tolerance), "Volume should scale by s^3");
        Assert.That(result.y, Is.EqualTo(24.0).Within(Tolerance), "Area should scale by s^2");
    }

    [Test]
    public void Volume_InvertedNormals_ReturnsNegativeVolume() {
        var mesh = CreateUnitCube();

        // Reverse the orientation of every triangle
        // This effectively turns the mesh "inside out"
        mesh.ReverseOrientation();

        // Act
        double volume = MeshMeasurementExtensions.Volume(mesh);

        // Assert
        // Volume should be -1.0 due to winding order
        Assert.That(volume, Is.EqualTo(-1.0).Within(Tolerance));
    }

    [Test]
    public void GetTriVertices_Extension_PopulatesTriangle3dCorrectly() {
        var mesh = new DMesh3();
        // Create a single triangle
        int v0 = mesh.AppendVertex(new Vector3d(0, 0, 0));
        int v1 = mesh.AppendVertex(new Vector3d(1, 0, 0));
        int v2 = mesh.AppendVertex(new Vector3d(0, 1, 0));
        int tid = mesh.AppendTriangle(v0, v1, v2);

        // Act
        mesh.GetTriVertices(tid, out Triangle3d tri);

        // Assert
        Assert.That(tri.V0, Is.EqualTo(new Vector3d(0, 0, 0)));
        Assert.That(tri.V1, Is.EqualTo(new Vector3d(1, 0, 0)));
        Assert.That(tri.V2, Is.EqualTo(new Vector3d(0, 1, 0)));
    }

    [Test]
    public void VolumeArea_DisconnectedMesh_SumsCorrectly() {
        // Create two unit cubes separated in space
        var mesh = CreateUnitCube();
        var mesh2 = CreateUnitCube();

        MeshTransforms.Translate(mesh2, new Vector3d(5, 0, 0)); // Move second cube away

        var editor = new MeshEditor(mesh);
        editor.AppendMesh(mesh2); // Combine them into one DMesh3

        // Act
        Vector2d result = MeshMeasurementExtensions.VolumeArea(mesh);

        // Assert
        // Total Volume = 1 + 1 = 2
        // Total Area = 6 + 6 = 12
        Assert.That(result.x, Is.EqualTo(2.0).Within(Tolerance));
        Assert.That(result.y, Is.EqualTo(12.0).Within(Tolerance));
    }
}

All the tests pass.

Vector3d V2mV0 = v2 - v0;
Vector3d N = V1mV0.Cross(V2mV0);

mesh.GetTriVertices(tid, out Triangle3d tri);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more efficient to have the out reference Triangle3d tri declared before the for loop so it's not created in each loop?

Triangle3d tri;
foreach(int tid in mesh.TriangleIndices())
{
    mesh.GetTriVerticies(tid, out tri);
....

{
double mass_integral = 0.0;
double area_sum = 0;
foreach (int tid in mesh.TriangleIndices())
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triangle3d tri;
foreach(int tid in mesh.TriangleIndices())
{
    mesh.GetTriVerticies(tid, out tri);
....

double tmp0 = v0.x + v1.x;
double f1x = tmp0 + v2.x;
mass_integral += N.x * f1x;
mesh.GetTriVertices(tid, out Triangle3d tri);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triangle3d tri;
foreach(int tid in mesh.TriangleIndices())
{
    mesh.GetTriVerticies(tid, out tri);
....

@rms80
Copy link
Contributor

rms80 commented Jan 9, 2026

I am pretty sure it won't make a difference in optimized code, because in both cases the function will run the constructor internally. Conceivably this way involves a copy while 'out Triangle3D tri' might be fully RVO'd (but possibly the copy is optimized away too). But you would have to inspect the IL to find out, and I doubt there would be a difference in profiling.

(personally I lean towards shorter code unless it really make a difference)

If you want maximal performance then iterating from 0 to MaxTriID and checking if the tri is valid in the loop is usually faster than using the foreach/enumerable (but again hard to be certain it would matter w/o profiling in Release)

@CLAassistant
Copy link

CLAassistant commented Jan 9, 2026

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants