From f7683f2c41a30b101b47aa2976c9b57ac12dbafd Mon Sep 17 00:00:00 2001 From: menes97 <38889017+menes97@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:10:22 +0300 Subject: [PATCH] Recompute mesh world bounds and grid intersections after changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NwcMesh kept its cached world bounds when Transform was replaced or removed, and NwcGridSystem kept its cached intersections after line endpoints were edited or Lines was changed directly, so bounds, grid intersections and location labels went stale. The Transform setter now clears the cached bounds, and the intersection cache, a small O(n²) pass over the grid lines, is removed. Fixes #100 Co-Authored-By: Claude Opus 5.5 --- src/NwcIO/Model/Grids/NwcGridSystem.cs | 9 ---- src/NwcIO/Model/NwcMesh.cs | 11 ++++- tests/NwcIO.Tests/GridSystemTests.cs | 62 ++++++++++++++++++++++++++ tests/NwcIO.Tests/MeshParserTests.cs | 50 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 10 deletions(-) diff --git a/src/NwcIO/Model/Grids/NwcGridSystem.cs b/src/NwcIO/Model/Grids/NwcGridSystem.cs index 3b9f958..bbf8e44 100644 --- a/src/NwcIO/Model/Grids/NwcGridSystem.cs +++ b/src/NwcIO/Model/Grids/NwcGridSystem.cs @@ -17,8 +17,6 @@ public sealed class NwcGridSystem public List Lines { get; } = []; - private List? _cachedIntersections; - public NwcGridSystem() { } public NwcGridSystem(string name) @@ -33,7 +31,6 @@ public NwcGridLine AddLine(NwcGridLine line) { ArgumentNullException.ThrowIfNull(line); Lines.Add(line); - _cachedIntersections = null; return line; } @@ -51,11 +48,6 @@ public NwcGridLine AddLine(string name, Vector3d start, Vector3d end, double ele /// public IReadOnlyList GetIntersections() { - if (_cachedIntersections is not null) - { - return _cachedIntersections; - } - var result = new List(); for (int i = 0; i < Lines.Count; i++) @@ -78,7 +70,6 @@ public IReadOnlyList GetIntersections() } } - _cachedIntersections = result; return result; } diff --git a/src/NwcIO/Model/NwcMesh.cs b/src/NwcIO/Model/NwcMesh.cs index 7c263d5..06f6607 100644 --- a/src/NwcIO/Model/NwcMesh.cs +++ b/src/NwcIO/Model/NwcMesh.cs @@ -23,7 +23,15 @@ public sealed class NwcMesh public BoundingBox3d? BoundingBox { get; init; } /// Optional 3D world transformation associated with this mesh instance. - public NwcTransform? Transform { get; set; } + public NwcTransform? Transform + { + get => _transform; + set + { + _transform = value; + _cachedWorldBounds = null; + } + } /// Surface material and color assigned to this mesh. public NwcMaterial? Material { get; set; } @@ -34,6 +42,7 @@ public sealed class NwcMesh /// Number of triangle faces in this mesh. public int TriangleCount => Indices.Count / 3; + private NwcTransform? _transform; private BoundingBox3d? _cachedWorldBounds; /// diff --git a/tests/NwcIO.Tests/GridSystemTests.cs b/tests/NwcIO.Tests/GridSystemTests.cs index 2a3c513..5f22df6 100644 --- a/tests/NwcIO.Tests/GridSystemTests.cs +++ b/tests/NwcIO.Tests/GridSystemTests.cs @@ -7,6 +7,68 @@ namespace NwcIO.Tests; public sealed class GridSystemTests { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GridSystem_AfterEndpointEdit_UpdatesIntersectionsAndLocation(bool editStart) + { + var grids = new NwcGridSystem(); + var line = grids.AddLine("A", new Vector3d(0, -10, 0), new Vector3d(0, 10, 0)); + grids.AddLine("B", new Vector3d(5, -10, 0), new Vector3d(5, 10, 0)); + grids.AddLine("1", new Vector3d(-10, 0, 0), new Vector3d(20, 0, 0)); + Assert.Equal(0.0, grids.GetIntersections()[0].IntersectionPoint.X); + Assert.Equal("B / 1", grids.FormatLocation(new Vector3d(5, 0, 0))); + + if (editStart) + { + line.Start = new Vector3d(20, -10, 0); + } + else + { + line.End = new Vector3d(20, 10, 0); + } + + var intersection = Assert.Single(grids.GetIntersections(), i => i.Line1Name == "A" && i.Line2Name == "1"); + Assert.Equal(10.0, intersection.IntersectionPoint.X); + Assert.Equal(0.0, intersection.IntersectionPoint.Y); + Assert.Equal("A / 1", grids.FormatLocation(new Vector3d(10, 0, 0))); + } + + [Fact] + public void GridSystem_AfterDirectLineAddition_UpdatesIntersectionsAndLocation() + { + var grids = new NwcGridSystem(); + grids.AddLine("A", new Vector3d(0, -10, 0), new Vector3d(0, 10, 0)); + grids.AddLine("1", new Vector3d(-10, 0, 0), new Vector3d(30, 0, 0)); + Assert.Single(grids.GetIntersections()); + Assert.Equal("A (+20.00m) / 1", grids.FormatLocation(new Vector3d(20, 0, 0))); + + grids.Lines.Add(new NwcGridLine("B", new Vector3d(20, -10, 0), new Vector3d(20, 10, 0))); + + Assert.Equal(2, grids.GetIntersections().Count); + var intersection = Assert.Single(grids.GetIntersections(), i => i.Line2Name == "B"); + Assert.Equal(20.0, intersection.IntersectionPoint.X); + Assert.Equal("1 / B", grids.FormatLocation(new Vector3d(20, 0, 0))); + } + + [Fact] + public void GridSystem_AfterDirectLineRemoval_UpdatesIntersectionsAndLocation() + { + var grids = new NwcGridSystem(); + var line = grids.AddLine("A", new Vector3d(0, -10, 0), new Vector3d(0, 10, 0)); + grids.AddLine("B", new Vector3d(20, -10, 0), new Vector3d(20, 10, 0)); + grids.AddLine("1", new Vector3d(-10, 0, 0), new Vector3d(30, 0, 0)); + Assert.Equal(2, grids.GetIntersections().Count); + Assert.Equal("A / 1", grids.FormatLocation(new Vector3d(0, 0, 0))); + + Assert.True(grids.Lines.Remove(line)); + + var intersection = Assert.Single(grids.GetIntersections()); + Assert.Equal("B / 1", intersection.DisplayName); + Assert.Equal(20.0, intersection.IntersectionPoint.X); + Assert.Equal("B (-20.00m) / 1", grids.FormatLocation(new Vector3d(0, 0, 0))); + } + [Fact] public void GridLine_CalculatesDistanceAndIntersection() { diff --git a/tests/NwcIO.Tests/MeshParserTests.cs b/tests/NwcIO.Tests/MeshParserTests.cs index fcca00a..e258350 100644 --- a/tests/NwcIO.Tests/MeshParserTests.cs +++ b/tests/NwcIO.Tests/MeshParserTests.cs @@ -8,6 +8,56 @@ namespace NwcIO.Tests; public class MeshParserTests { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Mesh_GetWorldBoundingBox_AfterTransformAssignment_UsesCurrentTransform(bool initiallyTransformed) + { + var mesh = new NwcMesh + { + Id = 1, + Vertices = [new Vector3d(0, 0, 0), new Vector3d(1, 2, 3)], + Indices = [], + BoundingBox = new BoundingBox3d(new Vector3d(0, 0, 0), new Vector3d(1, 2, 3)), + Transform = initiallyTransformed + ? new NwcTransform { Id = 1, Translation = new Vector3d(10, 20, 30) } + : null, + }; + + Assert.Equal(initiallyTransformed ? 10.0 : 0.0, mesh.GetWorldBoundingBox().Min.X); + + mesh.Transform = new NwcTransform { Id = 2, Translation = new Vector3d(100, 200, 300) }; + + var bounds = mesh.GetWorldBoundingBox(); + var vertices = mesh.GetWorldVertices(); + Assert.Equal(new Vector3d(100, 200, 300), bounds.Min); + Assert.Equal(new Vector3d(101, 202, 303), bounds.Max); + Assert.Equal(vertices[0], bounds.Min); + Assert.Equal(vertices[1], bounds.Max); + } + + [Fact] + public void Mesh_GetWorldBoundingBox_AfterTransformRemoval_UsesLocalBounds() + { + var mesh = new NwcMesh + { + Id = 1, + Vertices = [new Vector3d(0, 0, 0), new Vector3d(1, 2, 3)], + Indices = [], + BoundingBox = new BoundingBox3d(new Vector3d(0, 0, 0), new Vector3d(1, 2, 3)), + Transform = new NwcTransform { Id = 1, Translation = new Vector3d(100, 200, 300) }, + }; + + Assert.Equal(100.0, mesh.GetWorldBoundingBox().Min.X); + + mesh.Transform = null; + + var bounds = mesh.GetWorldBoundingBox(); + Assert.Equal(mesh.BoundingBox.Value, bounds); + Assert.Equal(mesh.GetWorldVertices()[0], bounds.Min); + Assert.Equal(mesh.GetWorldVertices()[1], bounds.Max); + } + [Fact] public void Mesh_TriangleCountAndIndices_AreCalculatedCorrectly() {