using System; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface.Controls { [Virtual] public class SpriteView : Control { protected SpriteSystem? SpriteSystem; private SharedTransformSystem? _transform; protected readonly IEntityManager EntMan; [ViewVariables] public SpriteComponent? Sprite => Entity?.Comp1; [ViewVariables] public Entity? Entity { get; private set; } [ViewVariables] public NetEntity? NetEnt { get; private set; } /// /// This field configures automatic scaling of the sprite. This automatic scaling is done before /// applying the explicitly set scale . /// public StretchMode Stretch { get; set; } = StretchMode.Fit; public enum StretchMode { /// /// Don't automatically scale the sprite. The sprite can still be scaled via /// None, /// /// Scales the sprite down so that it fits within the control. Does not scale the sprite up. Keeps the same /// aspect ratio. This automatic scaling is done before applying . /// Fit, /// /// Scale the sprite up or down so that it fills the whole control. Keeps the same aspect ratio. This /// automatic scaling is done before applying . /// Fill } /// /// Overrides the direction used to render the sprite. /// /// /// If null, the world space orientation of the entity will be used. Otherwise the specified direction will be /// used. /// public Direction? OverrideDirection { get; set; } #region Transform private Vector2 _scale = Vector2.One; private Angle _eyeRotation = Angle.Zero; private Angle? _worldRotation = Angle.Zero; public Angle EyeRotation { get => _eyeRotation; set { _eyeRotation = value; InvalidateMeasure(); } } /// /// Used to override the entity's world rotation. Note that the desired size of the control will not /// automatically get updated as the entity's world rotation changes. /// public Angle? WorldRotation { get => _worldRotation; set { _worldRotation = value; InvalidateMeasure(); } } /// /// Scale to apply when rendering the sprite. This is separate from the sprite's scale. /// public Vector2 Scale { get => _scale; set { _scale = value; InvalidateMeasure(); } } /// /// Cached desired size. Differs from as it it is not clamped by the /// minimum/maximum size options. /// private Vector2 _spriteSize; /// /// Determines whether or not the sprite's offset be applied to the control. /// public bool SpriteOffset { get; set; } #endregion public SpriteView() { IoCManager.Resolve(ref EntMan); RectClipContent = true; } public SpriteView(IEntityManager entMan) { EntMan = entMan; RectClipContent = true; } public SpriteView(EntityUid? uid, IEntityManager entMan) { EntMan = entMan; RectClipContent = true; SetEntity(uid); } public SpriteView(NetEntity uid, IEntityManager entMan) { EntMan = entMan; RectClipContent = true; SetEntity(uid); } public void SetEntity(NetEntity netEnt) { if (netEnt == NetEnt) return; // The Entity is getting set later in the ResolveEntity method // because the client may not have received it yet. Entity = null; NetEnt = netEnt; } public void SetEntity(EntityUid? uid) { if (Entity?.Owner == uid) return; if (!EntMan.TryGetComponent(uid, out SpriteComponent? sprite) || !EntMan.TryGetComponent(uid, out TransformComponent? xform)) { Entity = null; NetEnt = null; return; } Entity = new(uid.Value, sprite, xform); NetEnt = EntMan.GetNetEntity(uid); InvalidateMeasure(); } protected override Vector2 MeasureOverride(Vector2 availableSize) { // TODO Make this get called when sprite bounds/properties update? UpdateSize(); return _spriteSize; } private void UpdateSize() { if (!ResolveEntity(out _, out var sprite, out _)) return; var spriteBox = sprite.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation) .CalcBoundingBox(); if (!SpriteOffset) { // re-center the box. spriteBox = spriteBox.Translated(-spriteBox.Center); } // Scale the box (including any offset); var scale = _scale * EyeManager.PixelsPerMeter; var bl = spriteBox.BottomLeft * scale; var tr = spriteBox.TopRight * scale; // This view will be centered on (0,0). If the sprite was shifted by (1,2) the actual size of the control // would need to be at least (2,4). tr = Vector2.Max(tr, Vector2.Zero); bl = Vector2.Min(bl, Vector2.Zero); tr = Vector2.Max(tr, -bl); bl = Vector2.Min(bl, -tr); var box = new Box2(bl, tr); DebugTools.Assert(box.Contains(Vector2.Zero)); DebugTools.Assert(box.TopLeft.EqualsApprox(-box.BottomRight)); if (_worldRotation != null && _eyeRotation == Angle.Zero) // TODO This shouldn't need to be here, but I just give up at this point I am going fucking insane looking at rotating blobs of pixels. I doubt anyone will ever even use rotated sprite views.? { _spriteSize = box.Size; return; } // Size does not auto-update with world rotation. So if it is not fixed by _worldRotation we will just take // the maximum possible size. var size = box.Size; var longestSide = MathF.Max(size.X, size.Y); var longestRotatedSide = Math.Max(longestSide, (size.X + size.Y) / MathF.Sqrt(2)); _spriteSize = new Vector2(longestRotatedSide, longestRotatedSide); } protected internal override void Draw(IRenderHandle renderHandle) { if (!ResolveEntity(out var uid, out var sprite, out var xform)) return; SpriteSystem ??= EntMan.System(); _transform ??= EntMan.System(); // Ensure the sprite is animated despite possible not being visible in any viewport. SpriteSystem.ForceUpdate(uid); var stretchVec = Stretch switch { StretchMode.Fit => Vector2.Min(Size / _spriteSize, Vector2.One), StretchMode.Fill => Size / _spriteSize, _ => Vector2.One, }; var stretch = MathF.Min(stretchVec.X, stretchVec.Y); var offset = SpriteOffset ? Vector2.Zero : - (-_eyeRotation).RotateVec(sprite.Offset * _scale) * new Vector2(1, -1) * EyeManager.PixelsPerMeter; var position = PixelSize / 2 + offset * stretch * UIScale; var scale = Scale * UIScale * stretch; // control modulation is applied automatically to the screen handle, but here we need to use the world handle var world = renderHandle.DrawingHandleWorld; var oldModulate = world.Modulate; world.Modulate *= Modulate * ActualModulateSelf; renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, sprite, xform, _transform); world.Modulate = oldModulate; } private bool ResolveEntity( out EntityUid uid, [NotNullWhen(true)] out SpriteComponent? sprite, [NotNullWhen(true)] out TransformComponent? xform) { if (NetEnt != null && Entity == null && EntMan.TryGetEntity(NetEnt, out var ent)) SetEntity(ent); if (Entity != null) { (uid, sprite, xform) = Entity.Value; return !EntMan.Deleted(uid); } sprite = null; xform = null; uid = default; return false; } } }