From 407dc6cb8beac1e18dc92a92d2b469832e492722 Mon Sep 17 00:00:00 2001 From: Ruzihm Date: Thu, 22 Jan 2026 01:02:22 -0500 Subject: [PATCH 1/2] Uses matrix math to determine if a right-click falls inside the transformed bounding box of nearby sprites --- .../ContextMenu/ContextMenuPopup.xaml.cs | 39 ++++++++++++++++++- OpenDreamClient/Rendering/DreamIcon.cs | 30 +++++++++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index 3e90159083..0661a60439 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -3,6 +3,7 @@ using OpenDreamShared.Dream; using OpenDreamShared.Rendering; using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -23,6 +24,7 @@ internal sealed partial class ContextMenuPopup : Popup { private readonly DMISpriteSystem _spriteSystem; private readonly EntityLookupSystem _lookupSystem; private readonly MouseInputSystem _mouseInputSystem; + private readonly TransformSystem _transformSystem; private readonly EntityQuery _spriteQuery; private readonly EntityQuery _xformQuery; private readonly EntityQuery _mobSightQuery; @@ -40,6 +42,7 @@ public ContextMenuPopup() { _spriteSystem = _entitySystemManager.GetEntitySystem(); _lookupSystem = _entitySystemManager.GetEntitySystem(); _mouseInputSystem = _entitySystemManager.GetEntitySystem(); + _transformSystem = _entitySystemManager.GetEntitySystem(); _spriteQuery = _entityManager.GetEntityQuery(); _xformQuery = _entityManager.GetEntityQuery(); _mobSightQuery = _entityManager.GetEntityQuery(); @@ -49,8 +52,9 @@ public void RepopulateEntities(ScalingViewport viewport, Vector2 relativePos, Sc ContextMenu.RemoveAllChildren(); var mapCoords = viewport.ScreenToMap(pointerLocation.Position); - var entities = _lookupSystem.GetEntitiesInRange(mapCoords, 0.01f, LookupFlags.Uncontained | LookupFlags.Approximate); + // TODO: Even quite distant entities could have transformed bounding boxes that intersect with mapCoords. 10 tile radius is good for now... + var entities = _lookupSystem.GetEntitiesInRange(mapCoords, 10f, LookupFlags.Uncontained | LookupFlags.Approximate); foreach (var uid in entities) { if (_xformQuery.TryGetComponent(uid, out var transform) && !_entityManager.HasComponent(transform.ParentUid)) // Not a child of another entity continue; @@ -60,6 +64,8 @@ public void RepopulateEntities(ScalingViewport viewport, Vector2 relativePos, Sc continue; if (!_spriteSystem.IsVisible(sprite, transform, GetSeeInvisible(), null)) // Not invisible continue; + if (!IconTransformedBoundingBoxContainsPoint(transform!, sprite, mapCoords)) + continue; var reference = new ClientObjectReference(_entityManager.GetNetEntity(uid)); var name = _appearanceSystem.GetName(reference); @@ -95,6 +101,37 @@ public void RepopulateEntities(ScalingViewport viewport, Vector2 relativePos, Sc //because BYOND sure loves redundancy } + // Determines if the given point falls inside the transformed bounding box of the given sprite's icon and its entity transform + private bool IconTransformedBoundingBoxContainsPoint(TransformComponent transform, DMISpriteComponent sprite, MapCoordinates mapCoords) { + var worldPos = _transformSystem.GetWorldPosition(transform!); + + // Find center of icon in case it's not the same size as a tile + if (!sprite.Icon.TryGetSizeInTiles(out var size)) + return false; + + if (!sprite.Icon.TryGetOffsetInTiles(out var offset)) + return false; + + var centerWorldPos = worldPos + offset.Value + (size.Value - new Vector2(1, 1)) * 0.5f; + + // Get the icon's AABB centered at its worldcenter + Box2? iconAABB = null; + sprite.Icon.GetWorldAABB(centerWorldPos, ref iconAABB); + if (!iconAABB.HasValue) + return false; + + // Inverse transform on the point and check if it's in the icon's AABB + var iconTransform = sprite.Icon.Appearance!.Transform; + Matrix3x2 t = new(iconTransform[0], iconTransform[1], iconTransform[2], iconTransform[3], iconTransform[4], iconTransform[5]); + if (Matrix3x2.Invert(t, out var invT)) { + // Origin of transform is centerWorldPos so remove it before transforming, then put it back before checking + var xformedPoint = Vector2.Transform(mapCoords.Position - centerWorldPos, invT) + centerWorldPos; + return iconAABB.Value.Contains(xformedPoint); + } + + return false; + } + public void SetActiveItem(ContextMenuItem item) { if (_currentVerbMenu != null) { _currentVerbMenu.Close(); diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index 0d8be428c0..bda81c5a73 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -7,6 +7,7 @@ using Robust.Shared.Timing; using System.Linq; using OpenDreamClient.Interface; +using System.Diagnostics.Contracts; namespace OpenDreamClient.Rendering; @@ -199,14 +200,31 @@ private void EndAppearanceAnimation(AppearanceAnimation? appearanceAnimation) { } } - public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { - if (DMI != null && Appearance != null) { - Vector2 size = DMI.IconSize / (float)interfaceManager.IconSize; - Vector2 pixelOffset = Appearance.TotalPixelOffset / (float)interfaceManager.IconSize; + public bool TryGetSizeInTiles([NotNullWhen(true)] out Vector2? effectiveSize) { + if (DMI != null) { + effectiveSize = DMI.IconSize / (float)interfaceManager.IconSize; + return true; + } - worldPos += pixelOffset; + effectiveSize = null; + return false; + } + + public bool TryGetOffsetInTiles([NotNullWhen(true)] out Vector2? effectiveOffset) { + if (Appearance != null) { + effectiveOffset = Appearance!.TotalPixelOffset / (float)interfaceManager.IconSize; + return true; + } + + effectiveOffset = null; + return false; + } + + public void GetWorldAABB(Vector2 worldPos, ref Box2? aabb) { + if (TryGetOffsetInTiles(out var effectiveOffset) && TryGetSizeInTiles(out var size)) { + worldPos += effectiveOffset.Value; - Box2 thisAABB = Box2.CenteredAround(worldPos, size); + Box2 thisAABB = Box2.CenteredAround(worldPos, size.Value); aabb = aabb?.Union(thisAABB) ?? thisAABB; } From 9426ea92b74155325ef3f1ce4e4cf0fcc90f0738 Mon Sep 17 00:00:00 2001 From: Ruzihm Date: Thu, 22 Jan 2026 01:09:42 -0500 Subject: [PATCH 2/2] lint --- OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs | 2 +- OpenDreamClient/Rendering/DreamIcon.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index 0661a60439..27e984c257 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -103,7 +103,7 @@ public void RepopulateEntities(ScalingViewport viewport, Vector2 relativePos, Sc // Determines if the given point falls inside the transformed bounding box of the given sprite's icon and its entity transform private bool IconTransformedBoundingBoxContainsPoint(TransformComponent transform, DMISpriteComponent sprite, MapCoordinates mapCoords) { - var worldPos = _transformSystem.GetWorldPosition(transform!); + var worldPos = _transformSystem.GetWorldPosition(transform); // Find center of icon in case it's not the same size as a tile if (!sprite.Icon.TryGetSizeInTiles(out var size)) diff --git a/OpenDreamClient/Rendering/DreamIcon.cs b/OpenDreamClient/Rendering/DreamIcon.cs index bda81c5a73..0f5b7df7b6 100644 --- a/OpenDreamClient/Rendering/DreamIcon.cs +++ b/OpenDreamClient/Rendering/DreamIcon.cs @@ -7,7 +7,6 @@ using Robust.Shared.Timing; using System.Linq; using OpenDreamClient.Interface; -using System.Diagnostics.Contracts; namespace OpenDreamClient.Rendering;