Repository: motion-canvas/motion-canvas
File: packages/ui/src/components/viewport/Coordinates.tsx (TypeScript)

Code before:
import {InfoBox} from '../controls/InfoBox';
import {useCallback} from 'preact/hooks';
import {useDocumentEvent} from '../../hooks';
import {ViewportState} from './ViewportContext';

interface CoordinatesProps {
  visibility?: boolean;
  viewState: ViewportState;
  viewSize: DOMRectReadOnly;
}

export function Coordinates({
  visibility,
  viewState,
  viewSize,
}: CoordinatesProps) {
  const [mousePos, setMousePos] = useState({x: 0, y: 0});
  const [hover, setHover] = useState(false);

  useEffect(() => {
    const handleMouseMove = (event: {x: number; y: number}) => {
      // This selector probably needs to be improved to select the overlay more reliably (future proof).
      // Possibly use the internal state instead of a selector.
      const canvas = document.getElementsByTagName('canvas')[1];
      const rect = canvas.getBoundingClientRect();
      const xPos = Math.round(
        ((event.x - rect.x) / rect.width - 0.5) * canvas.width,
      );
      const yPos = Math.round(
        ((event.y - rect.y) / rect.height - 0.5) * canvas.height,
      );
      if (
        Math.abs(xPos) > canvas.width / 2 ||
        Math.abs(yPos) > canvas.height / 2
      ) {
        setHover?.(false);
      } else {
        setHover?.(true);
        // Only need to set position coordinates if hovering over the overlay
        let canvasPosX = event.x - viewSize.x;
        let canvasPosY = event.y - viewSize.y;

        canvasPosX -= viewState.x + viewState.width / 2;
        canvasPosY -= viewState.y + viewState.height / 2;
        canvasPosX /= viewState.zoom;
        canvasPosY /= viewState.zoom;

        canvasPosX = Math.round(canvasPosX);
        canvasPosY = Math.round(canvasPosY);

        setMousePos({

Review comment (suggestion):
The origin being located in the center of the scene is explicit to the `Scene2D` class.
Other scenes may implement a different coordinate system.

We should extend the `Inspectable` interface so that scenes can transform the mouse position however they want.
This would look something like this:
```ts
const scene = useCurrentScene();
if (isInspectable(scene)) {
  // calculate the absolute mouse position the same way as in EditorPreview.tsx:
  const absoluteCoords = ... 
  // use the new method on the Inspectable interface to transform it:
  const point = scene.transformMousePosition(absoluteCoords);
}
```

Code after:
import {useCallback, useContext} from 'preact/hooks';
import {
  useCurrentScene,
  useDocumentEvent,
  useSharedSettings,
} from '../../hooks';
import {ViewportContext} from './ViewportContext';
import {isInspectable} from '@motion-canvas/core';
import styles from '../controls/Controls.module.scss';

export function Coordinates() {
  const [mousePos, setMousePos] = useState({x: 0, y: 0});
  const [hover, setHover] = useState(true);

  const state = useContext(ViewportContext);
  const settings = {
    ...useSharedSettings(),
  };
  const scene = useCurrentScene();

  useEffect(() => {
    const handleMouseMove = (event: {x: number; y: number}) => {
      if (!isInspectable(scene)) return;
      const absoluteCoords = {
        x: event.x - state.size.x,
        y: event.y - state.size.y,
      };

      absoluteCoords.x -= state.x + state.width / 2;
      absoluteCoords.y -= state.y + state.height / 2;
      absoluteCoords.x /= state.zoom;
      absoluteCoords.y /= state.zoom;
      absoluteCoords.x += settings.size.width / 2;
      absoluteCoords.y += settings.size.height / 2;

      const point = scene.transformMousePosition(
        absoluteCoords.x,
        absoluteCoords.y,
      );

      point.x = Math.round(point.x);
      point.y = Math.round(point.y);

      // These coordinates are used purely for bounds detection of the preview window.
      const boundX = Math.round(
        ((event.x - state.size.x) / state.width - 0.5) * state.width,
      );
      const boundY = Math.round(
        ((event.y - state.size.y) / state.height - 0.5) * state.height,
      );
      if (