Repository: League-of-Foundry-Developers/foundry-vtt-types
Pull request #3171 (2025-02-15 23:54:48)

Types and tests for `ControlsLayer`, touch up Ping types 
The one error on this branch will go away as long as `minimal-layers-start` merges first


Patch:
diff --git a/src/foundry/client-esm/dice/roll.d.mts b/src/foundry/client-esm/dice/roll.d.mts
index 1623db626..19cfea07c 100644
--- a/src/foundry/client-esm/dice/roll.d.mts
+++ b/src/foundry/client-esm/dice/roll.d.mts
@@ -452,12 +452,13 @@ declare class Roll<D extends AnyObject = EmptyObject> {
    * @returns A promise which resolves to the created ChatMessage entity, if create is true
    *          or the Object of prepared chatData otherwise.
    */
+  // options: not null (destructured)
   toMessage<
     const Create extends boolean | null | undefined,
     ConcreteMessageData extends Roll.MessageData | null | undefined = undefined,
   >(
     messageData?: ConcreteMessageData,
-    options?: Roll.ToMessageOptions<Create>, // can't be null because it gets destructured
+    options?: Roll.ToMessageOptions<Create>,
   ): Promise<Roll.ToMessageReturn<Create, ConcreteMessageData>>;
 
   /* -------------------------------------------- */
@@ -589,10 +590,11 @@ declare namespace Roll {
   }
 
   type _ClassifyStringTermOptions = InexactPartial<{
-    /** Allow intermediate terms
-     *  @defaultValue true
+    /**
+     * Allow intermediate terms
+     * @defaultValue `true`
      */
-    intermediate: boolean; // not: null (default true)
+    intermediate: boolean | null;
 
     /** The prior classified term */
     prior: RollTerm | string | null;
diff --git a/src/foundry/client/data/documents/user.d.mts b/src/foundry/client/data/documents/user.d.mts
index dd41716f4..f24fe4031 100644
--- a/src/foundry/client/data/documents/user.d.mts
+++ b/src/foundry/client/data/documents/user.d.mts
@@ -1,4 +1,4 @@
-import type { InexactPartial } from "fvtt-types/utils";
+import type { InexactPartial, NullishProps } from "fvtt-types/utils";
 import type Document from "../../../common/abstract/document.d.mts";
 import type BaseUser from "../../../common/documents/user.d.mts";
 
@@ -17,26 +17,37 @@ declare global {
     type Schema = BaseUser.Schema;
     type Source = BaseUser.Source;
 
-    interface PingData {
+    /** @internal */
+    type _PingData = InexactPartial<{
       /**
-       * Pulls all connected clients' views to the pinged co-ordinates.
+       * The zoom level at which the ping was made.
+       * @defaultValue `1`
+       * @remarks Can't be `null` because it only has a parameter default and is eventually used as a divisor in `Canvas#_constrainView`
        */
-      pull?: false | undefined;
+      zoom: number;
+    }> &
+      NullishProps<{
+        /**
+         * Pulls all connected clients' views to the pinged co-ordinates.
+         */
+        pull: boolean;
 
-      /**
-       * The ping style, see CONFIG.Canvas.pings.
-       */
-      style: string;
+        /**
+         * The ping style, see CONFIG.Canvas.pings.
+         * @defaultValue `"pulse"`
+         * @remarks Overriden with `"arrow"` if the position of the ping is outside the viewport
+         *
+         * Overridden with `CONFIG.Canvas.pings.types.PULL` (`"chevron"` by default) if photosensitive mode is enabled and the ping is within the viewport
+         */
+        style: Ping.ConfiguredStyles;
+      }>;
 
+    /** @privateRemarks Only consumed by {@link ControlsLayer#handlePing} */
+    interface PingData extends _PingData {
       /**
        * The ID of the scene that was pinged.
        */
       scene: string;
-
-      /**
-       * The zoom level at which the ping was made.
-       */
-      zoom: number;
     }
 
     interface ActivityData {
@@ -44,7 +55,7 @@ declare global {
       sceneId: string | null;
 
       /** The position of the user's cursor. */
-      cursor: { x: number; y: number } | null;
+      cursor: PIXI.IPointData | null;
 
       /** The state of the user's ruler, if they are currently using one. */
       ruler: Ruler.MeasurementData | null;
diff --git a/src/foundry/client/pixi/board.d.mts b/src/foundry/client/pixi/board.d.mts
index 8808fe2d5..eb41da84a 100644
--- a/src/foundry/client/pixi/board.d.mts
+++ b/src/foundry/client/pixi/board.d.mts
@@ -530,7 +530,7 @@ declare global {
      * @param origin  - Point to display Ping at
      * @param options - Additional options to configure how the ping is drawn.
      */
-    ping(origin: Canvas.Point, options?: PingOptions): Promise<boolean>;
+    ping(origin: Canvas.Point, options?: Ping.ConstructorOptions): Promise<boolean>;
 
     /**
      * Create a BlurFilter instance and register it to the array for updates when the zoom level changes.
diff --git a/src/foundry/client/pixi/core/interaction/ping.d.mts b/src/foundry/client/pixi/core/interaction/ping.d.mts
index b9e787e9f..1738e2d94 100644
--- a/src/foundry/client/pixi/core/interaction/ping.d.mts
+++ b/src/foundry/client/pixi/core/interaction/ping.d.mts
@@ -1,4 +1,4 @@
-import type { InexactPartial } from "fvtt-types/utils";
+import type { IntentionalPartial, RemoveIndexSignatures } from "fvtt-types/utils";
 
 declare global {
   /**
@@ -9,9 +9,9 @@ declare global {
      * @param origin  - The canvas coordinates of the origin of the ping.
      * @param options - Additional options to configure the ping animation.
      */
-    constructor(origin: Canvas.Point, options?: PingOptions);
+    constructor(origin: Canvas.Point, options?: Ping.ConstructorOptions);
 
-    options: PingOptions;
+    options: Ping.ConstructorOptions;
 
     _color: Color;
 
@@ -38,13 +38,16 @@ declare global {
     interface Any extends AnyPing {}
     type AnyConstructor = typeof AnyPing;
 
+    type ConfiguredStyles = keyof RemoveIndexSignatures<typeof CONFIG.Canvas.pings.styles>;
+
     /** @internal */
-    type _Options = InexactPartial<{
+    type _ConstructorOptions = IntentionalPartial<{
       /**
        * The duration of the animation in milliseconds.
        * @defaultValue `900`
        * @remarks Can't be `null` because `options` is `mergeObject`ed with an object with this key,
-       * and the result is passed on to `CanvasAnimation.animate` in its options
+       * and the result is passed on to `CanvasAnimation.animate` in its options, which only has a
+       * parameter default for this property
        */
       duration: number | undefined;
 
@@ -69,11 +72,11 @@ declare global {
       /**
        * The name for the ping animation to pass to {@link CanvasAnimation.animate}.
        */
-      name?: PropertyKey | undefined | null;
+      name: PropertyKey | undefined | null;
     }>;
-  }
 
-  interface PingOptions extends Ping._Options {}
+    interface ConstructorOptions extends _ConstructorOptions {}
+  }
 }
 
 declare abstract class AnyPing extends Ping {
diff --git a/src/foundry/client/pixi/core/interaction/pings/chevron.d.mts b/src/foundry/client/pixi/core/interaction/pings/chevron.d.mts
index e0adb7bbc..32dd90585 100644
--- a/src/foundry/client/pixi/core/interaction/pings/chevron.d.mts
+++ b/src/foundry/client/pixi/core/interaction/pings/chevron.d.mts
@@ -10,7 +10,12 @@ declare global {
      * @param options - Additional options to configure the ping animation.
      *                  (default: `{duration: 900, size: 128, color: "#ff6400"}`)
      */
-    constructor(origin: Canvas.Point, options?: PingOptions);
+    constructor(
+      origin: Canvas.Point,
+
+      /** @privateRemarks Can't be `null` as it's directly assigned to  `Ping#options` which has properties accessed null-unsafely */
+      options?: Ping.ConstructorOptions,
+    );
 
     /** @defaultValue `(this.options.size / 2) * .75` */
     _r: number;
diff --git a/src/foundry/client/pixi/core/interaction/pings/pulse.d.mts b/src/foundry/client/pixi/core/interaction/pings/pulse.d.mts
index ffa11996c..3981b46d1 100644
--- a/src/foundry/client/pixi/core/interaction/pings/pulse.d.mts
+++ b/src/foundry/client/pixi/core/interaction/pings/pulse.d.mts
@@ -9,9 +9,14 @@ declare global {
      * @param origin - The canvas coordinates of the origin of the ping.
      * @param options - Additional options to configure the ping animation.
      */
-    constructor(origin: Canvas.Point, options?: PulsePingOptions);
+    constructor(
+      origin: Canvas.Point,
 
-    override options: PulsePingOptions;
+      /** @privateRemarks Can't be `null` as it's directly assigned to  `Ping#options` which has properties accessed null-unsafely */
+      options?: PulsePing.ConstructorOptions,
+    );
+
+    override options: PulsePing.ConstructorOptions;
 
     _color2: Color;
 
@@ -71,24 +76,24 @@ declare global {
     type AnyConstructor = typeof AnyPulsePing;
 
     /** @internal */
-    type _Options = InexactPartial<{
+    type _ConstructorOptions = InexactPartial<{
       /**
        * The number of rings used in the animation.
        * @defaultValue `3`
        * @remarks Can't be `null` as it only has a parameter default and a coerced `0` ring count is nonsensical
        */
-      rings?: number;
+      rings: number;
 
       /**
        * The alternate color that the rings begin at. Use white for a 'flashing' effect.
        * @defaultValue `#ffffff`
        * @remarks Can't be `null` as it only has a parameter default and `Color(NaN)`s are to be avoided
        */
-      color2?: Color.Source;
+      color2: Color.Source;
     }>;
-  }
 
-  interface PulsePingOptions extends PingOptions, PulsePing._Options {}
+    interface ConstructorOptions extends _ConstructorOptions, Ping.ConstructorOptions {}
+  }
 
   /**
    * A type of ping that produces an arrow pointing in a given direction.
@@ -98,10 +103,10 @@ declare global {
      * @param origin - The canvas coordinates of the origin of the ping. This becomes the arrow's tip.
      * @param options - Additional options to configure the ping animation.
      */
-    constructor(origin: PIXI.Point, options?: ArrowPing.Options);
+    constructor(origin: PIXI.Point, options?: ArrowPing.ConstructorOptions);
 
-    // @privateRemarks `options` does not get overridden here as the `rotation` key does not
-    // get passed up to super, so the property is still just `PulsePingOptions`
+    // @privateRemarks The `options` property does not get overridden here as the `rotation` key does not
+    // get passed up to super, so it's still just `PulsePing.ConstructorOptions`
 
     protected override _drawShape(g: PIXI.Graphics, color: number | Color, alpha: number, size: number): void;
   }
@@ -111,7 +116,7 @@ declare global {
     type AnyConstructor = typeof AnyArrowPing;
 
     /** @internal */
-    type _Options = NullishProps<{
+    type _ConstructorOptions = NullishProps<{
       /**
        * The angle of the arrow in radians.
        * @defaultValue `0`
@@ -120,7 +125,7 @@ declare global {
       rotation: number;
     }>;
 
-    interface Options extends PulsePingOptions, _Options {}
+    interface ConstructorOptions extends _ConstructorOptions, PulsePing.ConstructorOptions {}
   }
 
   /**
@@ -131,7 +136,7 @@ declare global {
      * @param origin  - The canvas coordinates of the origin of the ping.
      * @param options - Additional options to configure the ping animation.
      */
-    constructor(origin: PIXI.Point, options: AlertPing.Options);
+    constructor(origin: PIXI.Point, options: AlertPing.ConstructorOptions);
 
     protected override _drawShape(g: PIXI.Graphics, color: number | Color, alpha: number, size: number): void;
   }
@@ -141,14 +146,15 @@ declare global {
     type AnyConstructor = typeof AnyAlertPing;
 
     /** @privateRemarks Only exists to change the default value of `color` */
-    interface Options extends PulsePingOptions {
+    interface ConstructorOptions extends PulsePing.ConstructorOptions {
       /**
        * @defaultValue `"#ff0000"`
        * @remarks Can't be `null` or `undefined` because `options` is `mergeObject`ed with an object with this key,
        * and passing either to `Color.from` produces a `Color(NaN)`, which may cause breakage in subclasses or when
        * passed to PIXI methods
+       * @privateRemarks Typing this as `Ping.ConstructorOptions["color"]` breaks, because it thinks `| undefined` has snuck in for unknown reasons
        */
-      color?: PulsePingOptions["color"];
+      color?: Color.Source;
     }
   }
 }
diff --git a/src/foundry/client/pixi/layers/controls/layer.d.mts b/src/foundry/client/pixi/layers/controls/layer.d.mts
index 599b7ef6a..74a877ae2 100644
--- a/src/foundry/client/pixi/layers/controls/layer.d.mts
+++ b/src/foundry/client/pixi/layers/controls/layer.d.mts
@@ -1,10 +1,4 @@
-import type {
-  IntentionalPartial,
-  InexactPartial,
-  NullishProps,
-  RemoveIndexSignatures,
-  HandleEmptyObject,
-} from "../../../../../utils/index.d.mts";
+import type { FixedInstanceType, HandleEmptyObject, NullishProps } from "fvtt-types/utils";
 import type { LineIntersection } from "../../../../common/utils/geometry.d.mts";
 
 declare global {
@@ -24,62 +18,74 @@ declare global {
      */
     static get instance(): Canvas["controls"];
 
-    /**
-     * A container of DoorControl instances
-     * @defaultValue `this.addChild(new PIXI.Container())`
-     */
-    doors: PIXI.Container;
-
     /**
      * Always interactive even if disabled for doors controls
      * @defaultValue `true`
      */
     override interactiveChildren: boolean;
 
+    /**
+     * A container of DoorControl instances
+     * @defaultValue `new PIXI.Container()`
+     */
+    doors: PIXI.Container;
+
     /**
      * A container of cursor interaction elements.
      * Contains cursors, rulers, interaction rectangles, and pings
-     * @defaultValue `this.addChild(new PIXI.Container())`
+     * @defaultValue `new PIXI.Container()`
+     * @remarks This Container's `eventMode` is set to `"none"` and its `mask` is set to `canvas.masks.canvas`
      */
     cursors: PIXI.Container;
 
     /**
      * Ruler tools, one per connected user
-     * @defaultValue `this.addChild(new PIXI.Container())`
+     * @defaultValue `new PIXI.Container()`
+     * @remarks This Container's `eventMode` is set to `"none"`
      */
     rulers: PIXI.Container;
 
     /**
      * A graphics instance used for drawing debugging visualization
-     * @defaultValue `this.addChild(new PIXI.Graphics())`
+     * @defaultValue `new PIXI.Graphics()`
+     * @remarks This Graphics's `eventMode` is set to `"none"`
      */
     debug: PIXI.Graphics;
 
     /**
      * Canvas selection rectangle
-     * @defaultValue `undefined`
+     * @remarks Only `undefined` prior to first draw
      */
     select: PIXI.Graphics | undefined;
 
     /**
      * A mapping of user IDs to Cursor instances for quick access
      * @defaultValue `{}`
+     * @remarks Cursor class is non-configurable
      */
-    protected _cursors: Record<string, Cursor>;
+    _cursors: Record<string, Cursor>;
 
     /**
      * A convenience mapping of user IDs to Ruler instances for quick access
-     * @internal
      * @defaultValue `{}`
+     * @remarks Keys are User IDs
+     *
+     * Foundry marked `@private`
      */
-    protected _rulers: Record<string, Ruler>;
+    protected _rulers: Record<string, Ruler.ConfiguredInstance>;
 
     /**
      * The positions of any offscreen pings we are tracking.
-     * @internal
+     * @remarks Keys in the format `Ping.${foundry.utils.randomID()}`
+     *
+     * Foundry marked `@private`
      */
     protected _offscreenPings: Record<string, Canvas.Point>;
 
+    /**
+     * @privateRemarks This override does not exist in Foundry but reflects reality. Not automateable because of
+     * lack of access to the constructor from the instance side
+     */
     override options: ControlsLayer.LayerOptions;
 
     /**
@@ -96,16 +102,16 @@ declare global {
     /**
      * A convenience accessor to the Ruler for the active game user
      */
-    get ruler(): ReturnType<this["getRulerForUser"]>;
+    get ruler(): Ruler.ConfiguredInstance | null;
 
     /**
      * Get the Ruler display for a specific User ID
      */
-    getRulerForUser(userId: string): Ruler | null;
+    getRulerForUser(userId: string): Ruler.ConfiguredInstance | null;
 
-    override _draw(options: HandleEmptyObject<ControlsLayer.DrawOptions>): Promise<void>;
+    protected override _draw(options: HandleEmptyObject<ControlsLayer.DrawOptions>): Promise<void>;
 
-    override _tearDown(options: HandleEmptyObject<ControlsLayer.TearDownOptions>): Promise<void>;
+    protected override _tearDown(options: HandleEmptyObject<ControlsLayer.TearDownOptions>): Promise<void>;
 
     /**
      * Draw the cursors container
@@ -128,7 +134,7 @@ declare global {
      */
     drawSelect(coords: Canvas.Rectangle): void;
 
-    override _deactivate(): void;
+    protected override _deactivate(): void;
 
     /**
      * Handle mousemove events on the game canvas to broadcast activity of the user's cursor position
@@ -140,10 +146,11 @@ declare global {
      * @param event   - The triggering canvas interaction event.
      * @param origin  - The local canvas coordinates of the mousepress.
      */
-    protected _onLongPress(event: PIXI.FederatedEvent, origin: PIXI.Point): ReturnType<Canvas["ping"]>;
+    protected _onLongPress(event: PIXI.FederatedEvent, origin: PIXI.Point): Promise<boolean>;
 
     /**
      * Handle the canvas panning to a new view.
+     * @remarks Called externally in {@link Canvas#pan}
      */
     protected _onCanvasPan(): void;
 
@@ -162,8 +169,9 @@ declare global {
 
     /**
      * Update display of an active Ruler object for a user given provided data
+     * @see {@link Ruler#update}
      */
-    updateRuler(user: User.ConfiguredInstance, rulerData: Ruler.MeasurementData | null): void;
+    updateRuler(user: User.ConfiguredInstance, rulerData?: Ruler.MeasurementData | null): void;
 
     /**
      * Handle a broadcast ping.
@@ -176,11 +184,9 @@ declare global {
     handlePing(
       user: User.ConfiguredInstance,
       position: Canvas.Point,
-      /**
-       * @privateRemarks User.PingData is InexactPartial because `zoom` is assumed to be number
-       * PingOptions is IntentionalPartial because it gets `mergeObject`ed with some defaults */
-      data?: InexactPartial<User.PingData> & IntentionalPartial<PingOptions>,
-    ): ReturnType<this["drawPing"]>;
+      /** @remarks Despite being a `={}` parameter, an object containing a valid `scene` property (a scene ID) must be passed */
+      data: ControlsLayer.HandlePingOptions,
+    ): Promise<boolean>;
 
     /**
      * Draw a ping at the edge of the viewport, pointing to the location of an off-screen ping.
@@ -191,21 +197,8 @@ declare global {
      */
     drawOffscreenPing(
       position: Canvas.Point,
-      options?: /** @remarks Can't be NullishProps or InexactPartial because PingOptions gets spread into an object with existing values for some keys */
-      IntentionalPartial<PingOptions> &
-        NullishProps<{
-          /**
-           * The style of ping to draw, from CONFIG.Canvas.pings.
-           * @defaultValue `"arrow"`
-           */
-          style: keyof RemoveIndexSignatures<typeof CONFIG.Canvas.pings.styles>;
-
-          /**
-           * The user who pinged.
-           */
-          user: User.ConfiguredInstance;
-        }>,
-    ): ReturnType<this["drawPing"]>;
+      options?: ControlsLayer.DrawOffscreenPingOptions, // not:null (destructured)
+    ): Promise<boolean>;
 
     /**
      * Draw a ping on the canvas
@@ -215,39 +208,26 @@ declare global {
      * @returns A promise which resolves once the Ping has been drawn and animated
      */
     drawPing(
-      position: PIXI.Point,
-      options?: /** @remarks Can't be NullishProps or InexactPartial because PingOptions gets `mergeObject`ed */
-      IntentionalPartial<PingOptions> &
-        NullishProps<{
-          /**
-           * The style of ping to draw, from CONFIG.Canvas.pings.
-           * @defaultValue `"pulse"`
-           */
-          style: keyof RemoveIndexSignatures<typeof CONFIG.Canvas.pings.styles>;
-
-          /**
-           * The user who pinged.
-           */
-          user: User.ConfiguredInstance;
-        }>,
-    ): ReturnType<Ping["animate"]>;
+      position: Canvas.Point,
+      options?: ControlsLayer.DrawPingOptions, // not:null (destructured)
+    ): Promise<boolean>;
 
     /**
      * Given off-screen coordinates, determine the closest point at the edge of the viewport to these coordinates.
      * @param position - The off-screen co-ordinate.
      * @returns The closest point at the edge of the viewport to these coordinates and a ray cast from the centre of the screen towards it.
-     * @internal
+     * @remarks Foundry marked `@private`
      */
-    protected _findViewportIntersection(position: Canvas.Point): {
-      ray: Ray;
-
-      intersection: LineIntersection | null;
-    };
+    protected _findViewportIntersection(position: Canvas.Point): ControlsLayer.ViewportIntersectionData;
   }
 
   namespace ControlsLayer {
+    interface Any extends AnyControlsLayer {}
     type AnyConstructor = typeof AnyControlsLayer;
 
+    type ImplementationClass = CONFIG["Canvas"]["layers"]["controls"]["layerClass"];
+    type Implementation = FixedInstanceType<ImplementationClass>;
+
     interface DrawOptions extends InteractionLayer.DrawOptions {}
 
     interface TearDownOptions extends CanvasLayer.TearDownOptions {}
@@ -256,6 +236,36 @@ declare global {
       name: "controls";
       zIndex: 1000;
     }
+
+    interface HandlePingOptions extends User.PingData, Ping.ConstructorOptions {}
+
+    /** @internal */
+    type _DrawPingOptions = NullishProps<{
+      /**
+       * The user who pinged.
+       * @remarks Only used to set the color of the ping. If `user?.color` ends up `undefined`, the relevant Ping class will provide a default color.
+       * If a `color` property is passed along with this, `color` will take precedence
+       */
+      user: User.ConfiguredInstance;
+    }>;
+
+    interface DrawPingOptions extends _DrawPingOptions, Pick<User.PingData, "style">, Ping.ConstructorOptions {}
+
+    interface DrawOffscreenPingOptions extends DrawPingOptions {
+      /**
+       * @defaultValue `"arrow"`
+       * @remarks Can't be `null` as it only has a parameter default
+       */
+      style?: User.PingData["style"] | undefined;
+    }
+
+    interface ViewportIntersectionData {
+      /** A Ray from the center of the [viewport minus right sidebar] area to the point on the edge of that area in line with an offscreen ping */
+      ray: Ray;
+
+      /** The intersection with the viewport edge, or undefined if its on-screen */
+      intersection: LineIntersection | undefined;
+    }
   }
 }
 


Test patch:
diff --git a/tests/foundry/client/pixi/core/interaction/pings/pulse.test-d.ts b/tests/foundry/client/pixi/core/interaction/pings/pulse.test-d.ts
index bfbfb7266..dad3450d5 100644
--- a/tests/foundry/client/pixi/core/interaction/pings/pulse.test-d.ts
+++ b/tests/foundry/client/pixi/core/interaction/pings/pulse.test-d.ts
@@ -29,7 +29,7 @@ declare const someColor: Color;
 declare const someSymbol: unique symbol;
 
 const myAlertPing = new AlertPing(myPoint, {
-  color: undefined,
+  color: 0x00ff00,
   color2: someColor,
   rings: 15,
   name: someSymbol,
diff --git a/tests/foundry/client/pixi/layers/controls/layer.test-d.ts b/tests/foundry/client/pixi/layers/controls/layer.test-d.ts
index 44456a98e..0150a9c29 100644
--- a/tests/foundry/client/pixi/layers/controls/layer.test-d.ts
+++ b/tests/foundry/client/pixi/layers/controls/layer.test-d.ts
@@ -1,5 +1,107 @@
 import { expectTypeOf } from "vitest";
+import type { LineIntersection } from "../../../../../../src/foundry/common/utils/geometry.d.mts";
+
+expectTypeOf(ControlsLayer.instance).toMatchTypeOf<ControlsLayer.Implementation | undefined>();
+expectTypeOf(ControlsLayer.layerOptions).toEqualTypeOf<ControlsLayer.LayerOptions>();
 
 const layer = new ControlsLayer();
 
 expectTypeOf(layer.options.baseClass).toEqualTypeOf<typeof InteractionLayer>();
+expectTypeOf(layer.options).toEqualTypeOf<ControlsLayer.LayerOptions>();
+
+expectTypeOf(layer.doors).toEqualTypeOf<PIXI.Container>();
+expectTypeOf(layer.cursors).toEqualTypeOf<PIXI.Container>();
+expectTypeOf(layer.rulers).toEqualTypeOf<PIXI.Container>();
+expectTypeOf(layer.debug).toEqualTypeOf<PIXI.Graphics>();
+expectTypeOf(layer.select).toEqualTypeOf<PIXI.Graphics | undefined>();
+
+expectTypeOf(layer._cursors).toEqualTypeOf<Record<string, Cursor>>();
+expectTypeOf(layer["_rulers"]).toEqualTypeOf<Record<string, Ruler.ConfiguredInstance>>();
+expectTypeOf(layer["_offscreenPings"]).toEqualTypeOf<Record<string, Canvas.Point>>();
+
+expectTypeOf(layer.ruler).toEqualTypeOf<Ruler.ConfiguredInstance | null>();
+expectTypeOf(layer.getRulerForUser("afasfasg")).toEqualTypeOf<Ruler.ConfiguredInstance | null>();
+
+expectTypeOf(layer["_draw"]({})).toEqualTypeOf<Promise<void>>();
+expectTypeOf(layer["_tearDown"]({})).toEqualTypeOf<Promise<void>>();
+
+expectTypeOf(layer.drawCursors()).toBeVoid();
+expectTypeOf(layer.drawRulers()).toBeVoid();
+expectTypeOf(layer.drawDoors()).toBeVoid();
+
+declare const someRect: PIXI.ICanvasRect;
+expectTypeOf(layer.drawSelect(someRect)).toBeVoid();
+
+expectTypeOf(layer["_deactivate"]()).toBeVoid();
+expectTypeOf(layer["_onMouseMove"]()).toBeVoid();
+declare const someEvent: PIXI.FederatedEvent;
+declare const somePoint: PIXI.Point;
+expectTypeOf(layer["_onLongPress"](someEvent, somePoint)).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(layer["_onCanvasPan"]()).toBeVoid();
+
+declare const someUser: User.ConfiguredInstance;
+expectTypeOf(layer.drawCursor(someUser)).toEqualTypeOf<Cursor>();
+expectTypeOf(layer.updateCursor(someUser, somePoint)).toBeVoid();
+expectTypeOf(layer.updateCursor(someUser, null)).toBeVoid();
+
+expectTypeOf(layer.updateRuler(someUser)).toBeVoid();
+expectTypeOf(layer.updateRuler(someUser, null)).toBeVoid();
+expectTypeOf(
+  layer.updateRuler(someUser, {
+    destination: { x: 50, y: 50 },
+    history: [{ x: 20, y: 35, teleport: false, cost: 1 }],
+    state: Ruler.STATES.MEASURING,
+    token: "asfasgasg",
+    waypoints: [
+      { x: 0, y: 20 },
+      { x: 10, y: 35 },
+    ],
+  }),
+).toBeVoid();
+
+// @ts-expect-error handlePing requires a `scene` ID in its options
+expectTypeOf(layer.handlePing(someUser, somePoint)).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(layer.handlePing(someUser, somePoint, { scene: "some scene ID" })).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(
+  layer.handlePing(someUser, somePoint, {
+    scene: "some scene ID",
+    color: 0xfaddaf,
+    duration: 2323,
+    name: "SomePing.ASFASDFAS",
+    pull: null,
+    size: 120,
+    style: "alert",
+    zoom: 2.6,
+  }),
+).toEqualTypeOf<Promise<boolean>>();
+
+expectTypeOf(layer.drawOffscreenPing(somePoint)).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(layer.drawOffscreenPing(somePoint, {})).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(
+  layer.drawOffscreenPing(somePoint, {
+    color: [0.2, 0.7, 0.3],
+    duration: 500,
+    name: Symbol.toPrimitive,
+    size: 500,
+    style: "chevron",
+    user: someUser,
+  }),
+).toEqualTypeOf<Promise<boolean>>();
+
+expectTypeOf(layer.drawPing(somePoint)).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(layer.drawPing(somePoint, {})).toEqualTypeOf<Promise<boolean>>();
+expectTypeOf(
+  layer.drawPing(somePoint, {
+    // color: "#ABCDEF",
+    name: null,
+    duration: undefined,
+    size: 250,
+    style: null,
+    user: null,
+  }),
+).toEqualTypeOf<Promise<boolean>>();
+
+expectTypeOf(layer["_findViewportIntersection"](somePoint)).toEqualTypeOf<{
+  ray: Ray;
+  intersection: LineIntersection | undefined;
+}>();