Core/VirtualCamera.cs
using System.Threading.Tasks;
using Sandbox.Utility;

namespace MANIFOLD.Camera {
    public enum FOVMode { Vertical, Horizontal }
    
    /// <summary>
    /// Imagine a <see cref="CameraComponent"/> but it isn't real.
    /// </summary>
    [EditorHandle("materials/gizmo/virtual_cam.png")]
    [Title(LibraryData.TITLE_SPLIT + "Virtual Camera"), Icon("videocam"), Category(LibraryData.CATEGORY)]
    public sealed class VirtualCamera : Component, Component.ExecuteInEditor {
        /// <summary>
        /// This camera's priority in the stack.
        /// </summary>
        [Property]
        public int Priority { get; set; }
        
        /// <summary>
        /// Which type of field of view to use.
        /// <see cref="FOVMode.Vertical"/> is recommended.
        /// </summary>
        [Property, Header("Lens"), Title("FOV Type")]
        public FOVMode FOVMode { get; set; }
        /// <summary>
        /// Camera's field of view in degrees.
        /// </summary>
        [Property, Range(0, 180)]
        public float FieldOfView { get; set; } = 90f;
        
        /// <summary>
        /// Does this camera have an look target?
        /// If not, <see cref="TrackingTarget"/> is used.
        /// </summary>
        [Property, Header("Targets")]
        public bool UseLookTarget { get; set; }
        /// <summary>
        /// Target to track. Used as the <see cref="LookTarget"/> as well by default.
        /// </summary>
        [Property]
        public GameObject TrackingTarget { get; set; }
        /// <summary>
        /// Target to look at.
        /// </summary>
        [Property, ShowIf(nameof(UseLookTarget), true)]
        public GameObject LookTarget { get; set; }

        /// <summary>
        /// Should switching to this camera use a special transition?
        /// </summary>
        [Property, Header("Transition")]
        public bool UseCustomTransition { get; set; } = false;
        [ShowIf(nameof(UseCustomTransition), true)]
        [Property]
        public TransitionData TransitionData { get; set; }
        
        private CameraSystem internalSystem;
        private List<CameraExtension> extensions;
        private int lastComponentCount;
        
        public CameraSystem System {
            get {
                if (internalSystem == null) {
                    internalSystem = Scene.GetSystem<CameraSystem>();
                }
                return internalSystem;
            }
        }
        public bool IsActive => System.CurrentCamera == this;

        internal void OnSystemInit(CameraSystem system) {
            internalSystem = system;
            
            extensions = Components.GetAll<CameraExtension>(FindMode.EverythingInSelf).ToList();
            foreach (CameraExtension ext in extensions) {
                ext.Camera = this;
                ext.OnCameraInitialize();
            }
        }

        internal void DoExtensionUpdate(out Vector3 localPos, out Rotation localRot) {
            localPos = Vector3.Zero;
            localRot = Rotation.Identity;

            foreach (CameraExtension ext in extensions) {
                ext.OnCameraUpdate(ref localPos, ref localRot);
            }
        }

        protected override void OnStart() {
            if (internalSystem == null) {
                OnSystemInit(Scene.GetSystem<CameraSystem>());
            }
        }

        protected override void OnEnabled() {
            if (internalSystem == null) {
                OnSystemInit(Scene.GetSystem<CameraSystem>());
            }
            
            System.ActivateCamera(this);
        }

        protected override void OnDisabled() {
            System.DeactivateCamera(this);
        }

        protected override void OnUpdate() {
            if (!Scene.IsEditor) return; // we probably dont need to check this at runtime right?
            if (lastComponentCount != Components.Count) {
                extensions = Components.GetAll<CameraExtension>(FindMode.EverythingInSelf).ToList();
                Log.Info("component count has changed");
                lastComponentCount = Components.Count;
            }
        }
        
        protected override void DrawGizmos() {
            if (IsActive) return;
            if (!System.Brain.IsValid()) return;
            CameraComponent camera = System.Brain.Camera;

            float horizontalAngle = FieldOfView;
            // THIS IS WRONG BUT GOOD ENOUGH
            float verticalAngle = horizontalAngle * (camera.ScreenRect.Height / camera.ScreenRect.Width); // cam fov is horizontal

            Vector3 origin = Vector3.Zero;
            Vector3 forward = Vector3.Forward;
            Ray tl = new Ray(origin, forward * new Angles(verticalAngle * 0.5f, horizontalAngle * 0.5f, 0f));
            Ray tr = new Ray(origin, forward * new Angles(verticalAngle * 0.5f, -horizontalAngle * 0.5f, 0f));
            Ray bl = new Ray(origin, forward * new Angles(-verticalAngle * 0.5f, horizontalAngle * 0.5f, 0f));
            Ray br = new Ray(origin, forward * new Angles(-verticalAngle * 0.5f, -horizontalAngle * 0.5f, 0f));
            
            Frustum frustum = Frustum.FromCorners(tl, tr, br, bl, camera.ZNear, camera.ZFar);

            Gizmo.Draw.Color = new Color(0.5f, 0f, 0f);
            Gizmo.Draw.LineFrustum(frustum);
        }
    }
}