From e749d5ab4ee959a1f9e3413e4a773c03859706a7 Mon Sep 17 00:00:00 2001 From: Benjamin Alsbury-Nealy Date: Mon, 18 May 2026 07:33:04 -0400 Subject: [PATCH] Extend wall slabs past their ends to close corner gaps A wall slab ran exactly from its segment start to end, so two perpendicular walls meeting at an outer corner left a t/2 x t/2 square gap. Extend every slab by half a thickness at each end; perpendicular walls then overlap in the corner square. Doorway opening offsets shift by the extension to stay aligned with the wall. A doorway authored flush with a wall's start consequently gains a left jamb -- the corner stub now sits beside it -- so the affected BoundaryWallBuilder geometry test is updated to expect that. --- .../Geometry/BoundaryWallBuilder.cs | 23 +++++++++++++++---- .../Geometry/BoundaryWallBuilderTests.cs | 18 ++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/OpenApparatus.Core/Geometry/BoundaryWallBuilder.cs b/src/OpenApparatus.Core/Geometry/BoundaryWallBuilder.cs index 9149c46..76abc2c 100644 --- a/src/OpenApparatus.Core/Geometry/BoundaryWallBuilder.cs +++ b/src/OpenApparatus.Core/Geometry/BoundaryWallBuilder.cs @@ -50,7 +50,9 @@ static MeshData EmptyResult() static MeshData BuildClosed(Adjacency adj, float t, float h) { - var slab = SlabFrame.From(adj.SharedSegment, t); + // Extend the slab half a thickness past each end so perpendicular walls + // overlap at corners instead of leaving a t/2 square gap. + var slab = SlabFrame.From(Extend(adj.SharedSegment, t * 0.5f), t); var b = new MeshDataBuilder(); EmitClosedWallFaces(b, slab, h); b.EnsureSubmeshCount(SubmeshIndex.Count); @@ -85,11 +87,16 @@ static void EmitClosedWallFaces(MeshDataBuilder b, SlabFrame slab, float h) static MeshData BuildDoorway(Adjacency adj, float t, float h, Passage.Doorway door) { - var slab = SlabFrame.From(adj.SharedSegment, t); + // Extend the slab half a thickness past each end (see BuildClosed). + float ext = t * 0.5f; + var slab = SlabFrame.From(Extend(adj.SharedSegment, ext), t); const float EPS = 1e-5f; - // Sort openings by offset and validate fit + non-overlap. - var openings = new List(door.Openings); + // Sort openings by offset and validate fit + non-overlap. Offsets shift + // by the extension because the slab now starts ext before the segment. + var openings = new List(door.Openings.Count); + foreach (var op in door.Openings) + openings.Add(op.With(offsetAlongEdge: op.OffsetAlongEdge + ext)); openings.Sort((a, c) => a.OffsetAlongEdge.CompareTo(c.OffsetAlongEdge)); for (int i = 0; i < openings.Count; i++) { @@ -254,6 +261,14 @@ static void EmitBottomStrip(MeshDataBuilder b, SlabFrame slab, float xStart, flo slab.Corner(false, xStart, 0f)); } + /// Lengthens a segment by at each end, keeping + /// its direction — used to overlap walls at corners. + static EdgeSegment Extend(EdgeSegment seg, float d) + { + var dir = seg.Direction; + return new EdgeSegment(seg.Start - dir * d, seg.End + dir * d); + } + // -------------------- Slab frame helper -------------------- /// diff --git a/tests/OpenApparatus.Core.Tests/Geometry/BoundaryWallBuilderTests.cs b/tests/OpenApparatus.Core.Tests/Geometry/BoundaryWallBuilderTests.cs index b7357dc..730c3c4 100644 --- a/tests/OpenApparatus.Core.Tests/Geometry/BoundaryWallBuilderTests.cs +++ b/tests/OpenApparatus.Core.Tests/Geometry/BoundaryWallBuilderTests.cs @@ -106,21 +106,17 @@ public void DoorwayPassage_VertexCount_Matches15FacesAt4VertsEach() } [Fact] - public void DoorwayFlushWithStart_OmitsLeftJambAndLeftBottomAndLeftTunnelSide() + public void DoorwayAtSegmentStart_StillGetsLeftJamb_FromCornerExtension() { - // doorOffset=0 → no left jamb on either side, no left bottom rim. - // Tunnel left side stays (still bounds the door); right side stays; ceiling stays. - // Faces: 2 (right-jamb each side) + 1 (top) + 1 (right bottom only) + - // 2 (caps) + 3 (tunnel left/right/ceiling) = 9. Wait — both tunnel sides - // are still emitted because the door has finite width with two side walls. - // Reconsider: 2 (right jamb each side) + 1 (top) + 1 (right bottom only) + 2 (caps) + - // 3 (tunnel) + 0 (no lintel split) ... but lintel IS still present (door - // height < wall height). Lintel = 2 (one per side). - // Total: 2 + 2 + 1 + 1 + 2 + 3 = 11 faces. + // A doorway authored flush with the segment start (offset 0) is no + // longer flush with the rendered slab: each slab is extended half a + // thickness past its ends to close corner gaps, so a stub of wall sits + // to the left of the opening. The full jamb set is therefore emitted, + // giving the same 14 wall faces as an interior doorway. var (_, _, adj) = MakeInternalAdjacency( new Passage.Doorway(offsetAlongEdge: 0f, width: 0.4f, height: 2.2f)); var mesh = new BoundaryWallBuilder().Build(adj, 0.2f, 3f); - Assert.Equal(11 * 2, mesh.TriangleCount(SubmeshIndex.Walls)); + Assert.Equal(14 * 2, mesh.TriangleCount(SubmeshIndex.Walls)); Assert.Equal(1 * 2, mesh.TriangleCount(SubmeshIndex.Floor)); }