using System; using System.Linq; using System.Numerics; using Robust.Client.Graphics; using Robust.Shared.GameObjects; using Robust.Shared.Graphics.RSI; using Robust.Shared.Maths; using Robust.Shared.Utility; using static Robust.Client.GameObjects.SpriteComponent; namespace Robust.Client.GameObjects; // This partial class contains code related to updating a sprites bounding boxes and its position in the sprite tree. public sealed partial class SpriteSystem { /// /// Get a sprite's local bounding box. The returned bounds do factor in the sprite's scale but not the rotation or /// offset. /// public Box2 GetLocalBounds(Entity sprite) { if (!sprite.Comp.BoundsDirty) { DebugTools.Assert(sprite.Comp.Layers.All(x => !x.BoundsDirty || !x.Drawn)); return sprite.Comp._bounds; } var bounds = new Box2(); foreach (var layer in sprite.Comp.Layers) { if (layer.Drawn) bounds = bounds.Union(GetLocalBounds(layer)); } sprite.Comp._bounds = bounds.Scale(Vector2.Abs(sprite.Comp.Scale)); sprite.Comp.BoundsDirty = false; return sprite.Comp._bounds; } /// /// Get a layer's local bounding box relative to its owning sprite. Unlike the sprite variant of this method, this /// does account for the layer's rotation and offset. /// public Box2 GetLocalBounds(Layer layer) { if (!layer.BoundsDirty) { DebugTools.Assert(layer.Bounds.EqualsApprox(CalculateLocalBounds(layer))); return layer.Bounds; } layer.Bounds = CalculateLocalBounds(layer); layer.BoundsDirty = false; return layer.Bounds; } internal Box2 CalculateLocalBounds(Layer layer) { var textureSize = (Vector2) layer.PixelSize / EyeManager.PixelsPerMeter; var longestSide = MathF.Max(textureSize.X, textureSize.Y); var longestRotatedSide = Math.Max(longestSide, (textureSize.X + textureSize.Y) / MathF.Sqrt(2)); Vector2 size; var sprite = layer.Owner.Comp; // If this layer has any form of arbitrary rotation, return a bounding box big enough to cover // any possible rotation. if (layer._rotation != 0) { size = new Vector2(longestRotatedSide, longestRotatedSide); return Box2.CenteredAround(layer.Offset, size * Vector2.Abs(layer._scale)); } var snapToCardinals = sprite.SnapCardinals; if (sprite.GranularLayersRendering && layer.RenderingStrategy != LayerRenderingStrategy.UseSpriteStrategy) { snapToCardinals = layer.RenderingStrategy == LayerRenderingStrategy.SnapToCardinals; } if (snapToCardinals) { // Snapping to cardinals only makes sense for 1-directional layers/sprites DebugTools.Assert(layer._actualState == null || layer._actualState.RsiDirections == RsiDirectionType.Dir1); // We won't know the actual direction it snaps to, so we ahve to assume the box is given by the longest side. size = new Vector2(longestSide, longestSide); return Box2.CenteredAround(layer.Offset, size * Vector2.Abs(layer._scale)); } // Build the bounding box based on how many directions the sprite has size = (layer._actualState?.RsiDirections) switch { RsiDirectionType.Dir4 => new Vector2(longestSide, longestSide), RsiDirectionType.Dir8 => new Vector2(longestRotatedSide, longestRotatedSide), _ => textureSize }; return Box2.CenteredAround(layer.Offset, size * Vector2.Abs(layer._scale)); } /// /// Gets a sprite's bounding box in world coordinates. /// public Box2Rotated CalculateBounds(Entity sprite, Vector2 worldPos, Angle worldRot, Angle eyeRot) { // fast check for invisible sprites if (!sprite.Comp.Visible || sprite.Comp.Layers.Count == 0) return new Box2Rotated(new Box2(worldPos, worldPos), Angle.Zero, worldPos); // We need to modify world rotation so that it lies between 0 and 2pi. // This matters for 4 or 8 directional sprites deciding which quadrant (octant?) they lie in. // the 0->2pi convention is set by the sprite-rendering code that selects the layers. // See RenderInternal(). worldRot = worldRot.Reduced(); if (worldRot.Theta < 0) worldRot = new Angle(worldRot.Theta + Math.Tau); // Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We // could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually // transform our box by the combination of these matrices: var finalRotation = sprite.Comp.NoRotation ? sprite.Comp.Rotation - eyeRot : sprite.Comp.Rotation + worldRot; var bounds = GetLocalBounds(sprite); // slightly faster path if offset == 0 (true for 99.9% of sprites) if (sprite.Comp.Offset == Vector2.Zero) return new Box2Rotated(bounds.Translated(worldPos), finalRotation, worldPos); var adjustedOffset = sprite.Comp.NoRotation ? (-eyeRot).RotateVec(sprite.Comp.Offset) : worldRot.RotateVec(sprite.Comp.Offset); var position = adjustedOffset + worldPos; return new Box2Rotated(bounds.Translated(position), finalRotation, position); } private void DirtyBounds(Entity sprite) { sprite.Comp.BoundsDirty = true; foreach (var layer in sprite.Comp.Layers) { layer.BoundsDirty = true; } } }