From 4614c3dd5060ba80d230daa09571fb24a2acd02e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:14:06 +0000 Subject: [PATCH 1/4] Initial plan From 78b8f83c32ae34c61ccad35879605fce2b3bd1dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:33:45 +0000 Subject: [PATCH 2/4] Restrict portal shade matching to portal edges Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/b1a5cc85-7551-4a1d-923d-6c0da8a27227 Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com> --- .../PortalShadeMatchHelperTests.cs | 70 +++++++ TombLib/TombLib/LevelData/Compilers/Rooms.cs | 174 +++++++----------- .../TombLib/LevelData/Compilers/Structs.cs | 49 +++++ .../LevelData/Compilers/TombEngine/Rooms.cs | 104 ++++------- 4 files changed, 227 insertions(+), 170 deletions(-) create mode 100644 TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs diff --git a/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs new file mode 100644 index 0000000000..a64eccabfc --- /dev/null +++ b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs @@ -0,0 +1,70 @@ +using System.Numerics; +using System.Reflection; +using TombLib.LevelData.Compilers; +using TombLib; + +namespace TombLib.Test; + +[TestClass] +public class PortalShadeMatchHelperTests +{ + [TestMethod] + public void IsCandidate_RejectsInteriorPortalVertex() + { + var legacyPortalVertices = new[] + { + new tr_vertex(0, 0, 0), + new tr_vertex(4, 2, 0), + new tr_vertex(4, 2, 4), + new tr_vertex(0, 0, 4) + }; + + var tombEnginePortalVertices = new[] + { + new VectorInt3(0, 0, 0), + new VectorInt3(4, 2, 0), + new VectorInt3(4, 2, 4), + new VectorInt3(0, 0, 4) + }; + + Assert.IsFalse(InvokeLegacyCandidate(legacyPortalVertices, new tr_vertex(2, 1, 2))); + Assert.IsFalse(InvokeTombEngineCandidate(tombEnginePortalVertices, new Vector3(2.0f, 1.0f, 2.0f))); + } + + [TestMethod] + public void IsCandidate_AcceptsPortalEdgeVertex() + { + var legacyPortalVertices = new[] + { + new tr_vertex(0, 0, 0), + new tr_vertex(4, 2, 0), + new tr_vertex(4, 2, 4), + new tr_vertex(0, 0, 4) + }; + + var tombEnginePortalVertices = new[] + { + new VectorInt3(0, 0, 0), + new VectorInt3(4, 2, 0), + new VectorInt3(4, 2, 4), + new VectorInt3(0, 0, 4) + }; + + Assert.IsTrue(InvokeLegacyCandidate(legacyPortalVertices, new tr_vertex(2, 1, 0))); + Assert.IsTrue(InvokeTombEngineCandidate(tombEnginePortalVertices, new Vector3(2.0f, 1.0f, 0.0f))); + } + + private static bool InvokeLegacyCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) + { + var helperType = typeof(ShadeMatchSignature).Assembly.GetType("TombLib.LevelData.Compilers.PortalShadeMatchHelper", true)!; + var method = helperType.GetMethod("IsCandidate", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(tr_vertex[]), typeof(tr_vertex) }, null)!; + return (bool)method.Invoke(null, new object[] { portalVertices, vertexPosition })!; + } + + private static bool InvokeTombEngineCandidate(VectorInt3[] portalVertices, Vector3 vertexPosition) + { + var helperType = typeof(ShadeMatchSignature).Assembly.GetType("TombLib.LevelData.Compilers.PortalShadeMatchHelper", true)!; + var method = helperType.GetMethod("IsCandidate", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(VectorInt3[]), typeof(Vector3) }, null)!; + return (bool)method.Invoke(null, new object[] { portalVertices, vertexPosition })!; + } +} diff --git a/TombLib/TombLib/LevelData/Compilers/Rooms.cs b/TombLib/TombLib/LevelData/Compilers/Rooms.cs index 1224b8a2dc..f80e013b6f 100644 --- a/TombLib/TombLib/LevelData/Compilers/Rooms.cs +++ b/TombLib/TombLib/LevelData/Compilers/Rooms.cs @@ -1761,35 +1761,12 @@ private void MatchDoorShades(List roomList, tr_room room, bool grayscal (room.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate || otherRoom.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate))) { - int x1 = p.Vertices[0].X; - int y1 = p.Vertices[0].Y; - int z1 = p.Vertices[0].Z; - - int x2 = x1 + 1; - int y2 = y1 + 1; - int z2 = z1 + 1; - - for (int i = 1; i < 4; i++) - { - if (p.Vertices[i].X < x1) - x1 = p.Vertices[i].X; - else if (p.Vertices[i].X > x2) - x2 = p.Vertices[i].X + 1; - - if (p.Vertices[i].Y < y1) - y1 = p.Vertices[i].Y; - else if (p.Vertices[i].Y > y2) - y2 = p.Vertices[i].Y + 1; - - if (p.Vertices[i].Z < z1) - z1 = p.Vertices[i].Z; - else if (p.Vertices[i].Z > z2) - z2 = p.Vertices[i].Z + 1; - } - for (int i = 0; i < room.Vertices.Count; i++) { var v1 = room.Vertices[i]; + if (!PortalShadeMatchHelper.IsCandidate(p.Vertices, v1.Position)) + continue; + var sig = new ShadeMatchSignature() { // NOTE: We keep alternate group and water flag in dictionary as well, this way we only apply vertex colour to @@ -1799,94 +1776,85 @@ private void MatchDoorShades(List roomList, tr_room room, bool grayscal Position = new VectorInt3(v1.Position.X + room.Info.X, v1.Position.Y, v1.Position.Z + room.Info.Z) }; - if (v1.Position.X >= x1 && v1.Position.X <= x2) - if (v1.Position.Y >= y1 && v1.Position.Y <= y2) - if (v1.Position.Z >= z1 && v1.Position.Z <= z2) + v1.IsOnPortal = true; + room.Vertices[i] = v1; + + for (int j = 0; j < otherRoom.Vertices.Count; j++) + { + uint refColor = 0; + var v2 = otherRoom.Vertices[j]; + var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + + if (!isPresentInLookup) + { + if (_level.Settings.GameVersion != TRVersion.Game.TR5) { - v1.IsOnPortal = true; - room.Vertices[i] = v1; + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) + refColor = UnpackFrom24BitPair(v1.Lighting1, v1.Lighting2); + else + refColor = v1.Lighting2; + } + else + refColor = v1.Color; + } - int otherX = v1.Position.X + room.Info.X - otherRoom.Info.X; - int otherY = v1.Position.Y; - int otherZ = v1.Position.Z + room.Info.Z - otherRoom.Info.Z; + if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && + v1.Position.Y == v2.Position.Y && + room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + { + uint newColor = 0; - for (int j = 0; j < otherRoom.Vertices.Count; j++) - { - uint refColor = 0; - var v2 = otherRoom.Vertices[j]; - var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room + // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. + // This way we don't get sharp-cut half-transitioned vertex colour. - if (!isPresentInLookup) + if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) + { + var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; + + if (!_vertexColors.TryGetValue(baseSig, out newColor)) + { + if (_level.Settings.GameVersion != TRVersion.Game.TR5) { - if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - refColor = UnpackFrom24BitPair(v1.Lighting1, v1.Lighting2); - else - refColor = v1.Lighting2; - } + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) + newColor = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); else - refColor = v1.Color; + newColor = v2.Lighting2; } - - if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && - v1.Position.Y == v2.Position.Y && - room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + else + newColor = v2.Color; + } + } + else + { + if (grayscale) + newColor = (ushort)(8160 - (((8160 - v2.Lighting2) / 2) + ((8160 - refColor) / 2))); + else if (_level.Settings.GameVersion != TRVersion.Game.TR5) + { + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) { - uint newColor = 0; - - // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room - // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. - // This way we don't get sharp-cut half-transitioned vertex colour. - - if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) - { - var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; - - if (!_vertexColors.TryGetValue(baseSig, out newColor)) - { - if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - newColor = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); - else - newColor = v2.Lighting2; - } - else - newColor = v2.Color; - } - } - else - { - if (grayscale) - newColor = (ushort)(8160 - (((8160 - v2.Lighting2) / 2) + ((8160 - refColor) / 2))); - else if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - { - var color = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); - newColor = (uint)(0xff000000 | (((((color & 0xff) + (refColor & 0xff)) >> 1) | - 256 * (((((color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | - 256 * ((((color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); - } - else - newColor = (ushort)((((v2.Lighting2 & 0x1f) + (refColor & 0x1f)) >> 1) | - 32 * (((((v2.Lighting2 >> 5) & 0x1f) + ((refColor >> 5) & 0x1f)) >> 1) | - 32 * ((((v2.Lighting2 >> 10) & 0x1f) + ((refColor >> 10) & 0x1f)) >> 1))); - } - else - newColor = (uint)(0xff000000 | (((((v2.Color & 0xff) + (refColor & 0xff)) >> 1) | - 256 * (((((v2.Color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | - 256 * ((((v2.Color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); - } - - if (!isPresentInLookup) - _vertexColors.TryAdd(sig, newColor); - else - _vertexColors[sig] = newColor; + var color = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); + newColor = (uint)(0xff000000 | (((((color & 0xff) + (refColor & 0xff)) >> 1) | + 256 * (((((color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | + 256 * ((((color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); } + else + newColor = (ushort)((((v2.Lighting2 & 0x1f) + (refColor & 0x1f)) >> 1) | + 32 * (((((v2.Lighting2 >> 5) & 0x1f) + ((refColor >> 5) & 0x1f)) >> 1) | + 32 * ((((v2.Lighting2 >> 10) & 0x1f) + ((refColor >> 10) & 0x1f)) >> 1))); } + else + newColor = (uint)(0xff000000 | (((((v2.Color & 0xff) + (refColor & 0xff)) >> 1) | + 256 * (((((v2.Color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | + 256 * ((((v2.Color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); } + + if (!isPresentInLookup) + _vertexColors.TryAdd(sig, newColor); + else + _vertexColors[sig] = newColor; + } + } } } } diff --git a/TombLib/TombLib/LevelData/Compilers/Structs.cs b/TombLib/TombLib/LevelData/Compilers/Structs.cs index 6412d85081..bd9f7b78b2 100644 --- a/TombLib/TombLib/LevelData/Compilers/Structs.cs +++ b/TombLib/TombLib/LevelData/Compilers/Structs.cs @@ -32,6 +32,55 @@ public override int GetHashCode() public override bool Equals(object obj) => GetHashCode() == obj.GetHashCode(); } + internal static class PortalShadeMatchHelper + { + public static bool IsCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) + { + return IsCandidate( + new Vector3(vertexPosition.X, vertexPosition.Y, vertexPosition.Z), + new Vector3(portalVertices[0].X, portalVertices[0].Y, portalVertices[0].Z), + new Vector3(portalVertices[1].X, portalVertices[1].Y, portalVertices[1].Z), + new Vector3(portalVertices[2].X, portalVertices[2].Y, portalVertices[2].Z), + new Vector3(portalVertices[3].X, portalVertices[3].Y, portalVertices[3].Z)); + } + + public static bool IsCandidate(global::TombLib.VectorInt3[] portalVertices, Vector3 vertexPosition) + { + return IsCandidate( + vertexPosition, + new Vector3(portalVertices[0].X, portalVertices[0].Y, portalVertices[0].Z), + new Vector3(portalVertices[1].X, portalVertices[1].Y, portalVertices[1].Z), + new Vector3(portalVertices[2].X, portalVertices[2].Y, portalVertices[2].Z), + new Vector3(portalVertices[3].X, portalVertices[3].Y, portalVertices[3].Z)); + } + + private static bool IsCandidate(Vector3 vertexPosition, Vector3 vertex0, Vector3 vertex1, Vector3 vertex2, Vector3 vertex3) + { + return IsPointOnSegment(vertexPosition, vertex0, vertex1) || + IsPointOnSegment(vertexPosition, vertex1, vertex2) || + IsPointOnSegment(vertexPosition, vertex2, vertex3) || + IsPointOnSegment(vertexPosition, vertex3, vertex0); + } + + private static bool IsPointOnSegment(Vector3 vertexPosition, Vector3 segmentStart, Vector3 segmentEnd) + { + const float epsilon = 0.001f; + + var segment = segmentEnd - segmentStart; + var offset = vertexPosition - segmentStart; + var segmentLengthSquared = segment.LengthSquared(); + + if (segmentLengthSquared <= float.Epsilon) + return false; + + var projection = Vector3.Dot(offset, segment); + if (projection < -epsilon || projection > segmentLengthSquared + epsilon) + return false; + + return Vector3.Cross(offset, segment).LengthSquared() <= epsilon * epsilon * segmentLengthSquared; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct tr_color { diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs index 86ce048b32..edb598a105 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs @@ -1648,35 +1648,12 @@ private void MatchDoorShades(List roomList, TombEngineRoom room, (room.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate || otherRoom.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate))) { - int x1 = p.Vertices[0].X; - int y1 = p.Vertices[0].Y; - int z1 = p.Vertices[0].Z; - - int x2 = x1 + 1; - int y2 = y1 + 1; - int z2 = z1 + 1; - - for (int i = 1; i < 4; i++) - { - if (p.Vertices[i].X < x1) - x1 = p.Vertices[i].X; - else if (p.Vertices[i].X > x2) - x2 = p.Vertices[i].X + 1; - - if (p.Vertices[i].Y < y1) - y1 = p.Vertices[i].Y; - else if (p.Vertices[i].Y > y2) - y2 = p.Vertices[i].Y + 1; - - if (p.Vertices[i].Z < z1) - z1 = p.Vertices[i].Z; - else if (p.Vertices[i].Z > z2) - z2 = p.Vertices[i].Z + 1; - } - for (int i = 0; i < room.Vertices.Count; i++) { var v1 = room.Vertices[i]; + if (!PortalShadeMatchHelper.IsCandidate(p.Vertices, v1.Position)) + continue; + var sig = new ShadeMatchSignature() { // NOTE: We keep alternate group and water flag in dictionary as well, this way we only apply vertex colour to @@ -1686,51 +1663,44 @@ private void MatchDoorShades(List roomList, TombEngineRoom room, Position = new VectorInt3((int)v1.Position.X + room.Info.X, (int)v1.Position.Y, (int)v1.Position.Z + room.Info.Z) }; - if (v1.Position.X >= x1 && v1.Position.X <= x2) - if (v1.Position.Y >= y1 && v1.Position.Y <= y2) - if (v1.Position.Z >= z1 && v1.Position.Z <= z2) - { - v1.IsOnPortal = true; - room.Vertices[i] = v1; - - int otherX = (int)v1.Position.X + room.Info.X - otherRoom.Info.X; - int otherY = (int)v1.Position.Y; - int otherZ = (int)v1.Position.Z + room.Info.Z - otherRoom.Info.Z; - - for (int j = 0; j < otherRoom.Vertices.Count; j++) - { - var v2 = otherRoom.Vertices[j]; - Vector3 refColor; - var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); - if (!isPresentInLookup) refColor = v1.Color; - - if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && - v1.Position.Y == v2.Position.Y && - room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) - { - Vector3 newColor; + v1.IsOnPortal = true; + room.Vertices[i] = v1; - // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room - // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. - // This way we don't get sharp-cut half-transitioned vertex colour. + for (int j = 0; j < otherRoom.Vertices.Count; j++) + { + var v2 = otherRoom.Vertices[j]; + Vector3 refColor; + var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + if (!isPresentInLookup) + refColor = v1.Color; + + if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && + v1.Position.Y == v2.Position.Y && + room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + { + Vector3 newColor; - if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) - { - var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; - if (!_vertexColors.TryGetValue(baseSig, out newColor)) newColor = v2.Color; - } - else - { - newColor = (v2.Color + refColor) / 2.0f; - } + // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room + // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. + // This way we don't get sharp-cut half-transitioned vertex colour. - if (!isPresentInLookup) - _vertexColors.TryAdd(sig, newColor); - else - _vertexColors[sig] = newColor; - } - } + if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) + { + var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; + if (!_vertexColors.TryGetValue(baseSig, out newColor)) + newColor = v2.Color; + } + else + { + newColor = (v2.Color + refColor) / 2.0f; } + + if (!isPresentInLookup) + _vertexColors.TryAdd(sig, newColor); + else + _vertexColors[sig] = newColor; + } + } } } } From f875bb71302abb4540f6c171bbe6e812a9bd3006 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:35:39 +0000 Subject: [PATCH 3/4] Polish portal shade matching helper tests Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/b1a5cc85-7551-4a1d-923d-6c0da8a27227 Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com> --- TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs | 11 +++-------- TombLib/TombLib/LevelData/Compilers/Structs.cs | 8 ++++---- TombLib/TombLib/Properties/AssemblyInfo.cs | 2 ++ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs index a64eccabfc..4b5acaebb1 100644 --- a/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs +++ b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs @@ -1,7 +1,6 @@ using System.Numerics; -using System.Reflection; -using TombLib.LevelData.Compilers; using TombLib; +using TombLib.LevelData.Compilers; namespace TombLib.Test; @@ -56,15 +55,11 @@ public void IsCandidate_AcceptsPortalEdgeVertex() private static bool InvokeLegacyCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) { - var helperType = typeof(ShadeMatchSignature).Assembly.GetType("TombLib.LevelData.Compilers.PortalShadeMatchHelper", true)!; - var method = helperType.GetMethod("IsCandidate", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(tr_vertex[]), typeof(tr_vertex) }, null)!; - return (bool)method.Invoke(null, new object[] { portalVertices, vertexPosition })!; + return PortalShadeMatchHelper.IsCandidate(portalVertices, vertexPosition); } private static bool InvokeTombEngineCandidate(VectorInt3[] portalVertices, Vector3 vertexPosition) { - var helperType = typeof(ShadeMatchSignature).Assembly.GetType("TombLib.LevelData.Compilers.PortalShadeMatchHelper", true)!; - var method = helperType.GetMethod("IsCandidate", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(VectorInt3[]), typeof(Vector3) }, null)!; - return (bool)method.Invoke(null, new object[] { portalVertices, vertexPosition })!; + return PortalShadeMatchHelper.IsCandidate(portalVertices, vertexPosition); } } diff --git a/TombLib/TombLib/LevelData/Compilers/Structs.cs b/TombLib/TombLib/LevelData/Compilers/Structs.cs index bd9f7b78b2..d87bdfc078 100644 --- a/TombLib/TombLib/LevelData/Compilers/Structs.cs +++ b/TombLib/TombLib/LevelData/Compilers/Structs.cs @@ -34,6 +34,8 @@ public override int GetHashCode() internal static class PortalShadeMatchHelper { + private const float PortalEdgeEpsilon = 0.001f; + public static bool IsCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) { return IsCandidate( @@ -64,8 +66,6 @@ private static bool IsCandidate(Vector3 vertexPosition, Vector3 vertex0, Vector3 private static bool IsPointOnSegment(Vector3 vertexPosition, Vector3 segmentStart, Vector3 segmentEnd) { - const float epsilon = 0.001f; - var segment = segmentEnd - segmentStart; var offset = vertexPosition - segmentStart; var segmentLengthSquared = segment.LengthSquared(); @@ -74,10 +74,10 @@ private static bool IsPointOnSegment(Vector3 vertexPosition, Vector3 segmentStar return false; var projection = Vector3.Dot(offset, segment); - if (projection < -epsilon || projection > segmentLengthSquared + epsilon) + if (projection < -PortalEdgeEpsilon || projection > segmentLengthSquared + PortalEdgeEpsilon) return false; - return Vector3.Cross(offset, segment).LengthSquared() <= epsilon * epsilon * segmentLengthSquared; + return Vector3.Cross(offset, segment).LengthSquared() <= PortalEdgeEpsilon * PortalEdgeEpsilon * segmentLengthSquared; } } diff --git a/TombLib/TombLib/Properties/AssemblyInfo.cs b/TombLib/TombLib/Properties/AssemblyInfo.cs index 66c604f97b..276fea7b34 100644 --- a/TombLib/TombLib/Properties/AssemblyInfo.cs +++ b/TombLib/TombLib/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -14,6 +15,7 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: SupportedOSPlatform("windows")] +[assembly: InternalsVisibleTo("TombLib.Test")] // Se si imposta ComVisible su false, i tipi in questo assembly non saranno visibili // ai componenti COM. Se è necessario accedere a un tipo in questo assembly da From ca71ec4d268bc651f51c29c651fabe4a9f778740 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:37:13 +0000 Subject: [PATCH 4/4] Document portal edge shade matching tolerance Agent-Logs-Url: https://github.com/TombEngine/Tomb-Editor/sessions/b1a5cc85-7551-4a1d-923d-6c0da8a27227 Co-authored-by: Nickelony <20436882+Nickelony@users.noreply.github.com> --- TombLib/TombLib/LevelData/Compilers/Structs.cs | 1 + TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/TombLib/TombLib/LevelData/Compilers/Structs.cs b/TombLib/TombLib/LevelData/Compilers/Structs.cs index d87bdfc078..22f4a0a546 100644 --- a/TombLib/TombLib/LevelData/Compilers/Structs.cs +++ b/TombLib/TombLib/LevelData/Compilers/Structs.cs @@ -34,6 +34,7 @@ public override int GetHashCode() internal static class PortalShadeMatchHelper { + // Tolerance for point-on-segment checks to absorb floating-point error. private const float PortalEdgeEpsilon = 0.001f; public static bool IsCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs index edb598a105..a291da76bd 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs @@ -1651,6 +1651,7 @@ private void MatchDoorShades(List roomList, TombEngineRoom room, for (int i = 0; i < room.Vertices.Count; i++) { var v1 = room.Vertices[i]; + // Only match shades for vertices that actually lie on the portal edge. if (!PortalShadeMatchHelper.IsCandidate(p.Vertices, v1.Position)) continue;