Commit 94b4cdd3 authored by Grant's avatar Grant
Browse files

rewrite router (related #33)

parent 634a69e7
Loading
Loading
Loading
Loading
+46 −63
Original line number Diff line number Diff line
@@ -5,9 +5,9 @@ import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer";
import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext";
import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom";
import throttle from "lodash.throttle";
import { Routes } from "../lib/routes";
import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net";
import { IPosition } from "@sc07-canvas/lib/src/net";
import { Template } from "./Template";
import { IRouterData, Router } from "../lib/router";

export const CanvasWrapper = () => {
  // to prevent safari from blurring things, use the zoom css property
@@ -21,79 +21,60 @@ export const CanvasWrapper = () => {
  );
};

const parseHashParams = (canvas: Canvas) => {
  // maybe move this to a utility inside routes.ts

  let { hash } = new URL(window.location.href);
  if (hash.indexOf("#") === 0) {
    hash = hash.slice(1);
  }
  let params = new URLSearchParams(hash);

  let position: {
    x?: number;
    y?: number;
    zoom?: number;
  } = {};

  if (params.has("x") && !isNaN(parseInt(params.get("x")!)))
    position.x = parseInt(params.get("x")!);
  if (params.has("y") && !isNaN(parseInt(params.get("y")!)))
    position.y = parseInt(params.get("y")!);
  if (params.has("zoom") && !isNaN(parseInt(params.get("zoom")!)))
    position.zoom = parseInt(params.get("zoom")!);

  if (
    typeof position.x === "number" &&
    typeof position.y === "number" &&
    typeof position.zoom === "number"
  ) {
    const { transformX, transformY } = canvas.canvasToPanZoomTransform(
      position.x,
      position.y
    );

    return {
      x: transformX,
      y: transformY,
      zoom: position.zoom,
    };
  }
};

const CanvasInner = () => {
  const canvasRef = createRef<HTMLCanvasElement>();
  const { config, setCanvasPosition, setCursorPosition } = useAppContext();
  const PanZoom = useContext(RendererContext);

  useEffect(() => {
    Router.PanZoom = PanZoom;

    // @ts-ignore
    window.TEST_Router = Router;
  }, [PanZoom]);

  useEffect(() => {
    if (!config.canvas || !canvasRef.current) return;
    const canvas = canvasRef.current!;
    const canvasInstance = new Canvas(config, canvas, PanZoom);
    const initAt = Date.now();

    {
      // TODO: handle hash changes and move viewport
      // NOTE: this will need to be cancelled if handleViewportMove was executed recently
    const handleNavigate = (data: IRouterData) => {
      if (data.canvas) {
        const position = canvasInstance.canvasToPanZoomTransform(
          data.canvas.x,
          data.canvas.y
        );

      const position = parseHashParams(canvasInstance);
      if (position) {
        PanZoom.setPosition(position, { suppressEmit: true });
      }
        PanZoom.setPosition(
          {
            x: position.transformX,
            y: position.transformY,
            zoom: data.canvas.zoom || 0, // TODO: fit canvas to viewport instead of defaulting
          },
          { suppressEmit: true }
        );
      }

    const handleViewportMove = throttle((state: ViewportMoveEvent) => {
      const pos = canvasInstance.panZoomTransformToCanvas();

      const canvasPosition: ICanvasPosition = {
        x: pos.canvasX,
        y: pos.canvasY,
        zoom: state.scale >> 0,
    };

      setCanvasPosition(canvasPosition);
    // initial load
    const initialRouter = Router.get();
    console.log(
      "[CanvasWrapper] Initial router data, handling navigate",
      initialRouter
    );
    handleNavigate(initialRouter);

      window.location.replace(Routes.canvas({ pos: canvasPosition }));
    }, 1000);
    const handleViewportMove = (state: ViewportMoveEvent) => {
      if (Date.now() - initAt < 60 * 1000) {
        console.debug(
          "[CanvasWrapper] handleViewportMove called soon after init",
          Date.now() - initAt
        );
      }

      Router.queueUpdate();
    };

    const handleCursorPos = throttle((pos: IPosition) => {
      if (
@@ -111,11 +92,13 @@ const CanvasInner = () => {

    PanZoom.addListener("viewportMove", handleViewportMove);
    canvasInstance.on("cursorPos", handleCursorPos);
    Router.on("navigate", handleNavigate);

    return () => {
      canvasInstance.destroy();
      PanZoom.removeListener("viewportMove", handleViewportMove);
      canvasInstance.off("cursorPos", handleCursorPos);
      Router.off("navigate", handleNavigate);
    };

    // ! do not include canvasRef, it causes infinite re-renders
+41 −6
Original line number Diff line number Diff line
import { PropsWithChildren, createContext, useContext, useState } from "react";
import {
  PropsWithChildren,
  createContext,
  useContext,
  useEffect,
  useState,
} from "react";
import { IRouterData, Router } from "../lib/router";

interface ITemplate {
  /**
@@ -35,13 +42,41 @@ const templateContext = createContext<ITemplate>({} as any);
export const useTemplateContext = () => useContext(templateContext);

export const TemplateContext = ({ children }: PropsWithChildren) => {
  const [enable, setEnable] = useState(false);
  const [url, setURL] = useState<string>();
  const [width, setWidth] = useState<number>();
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  const routerData = Router.get();
  const [enable, setEnable] = useState(!!routerData.template?.url);
  const [url, setURL] = useState<string | undefined>(routerData.template?.url);
  const [width, setWidth] = useState<number | undefined>(
    routerData.template?.width
  );
  const [x, setX] = useState(routerData.template?.x || 0);
  const [y, setY] = useState(routerData.template?.y || 0);
  const [opacity, setOpacity] = useState(100);

  useEffect(() => {
    const handleNavigate = (data: IRouterData) => {
      if (data.template) {
        setEnable(true);
        setURL(data.template.url);
        setWidth(data.template.width);
        setX(data.template.x || 0);
        setY(data.template.y || 0);
      } else {
        setEnable(false);
      }
    };

    Router.on("navigate", handleNavigate);

    return () => {
      Router.off("navigate", handleNavigate);
    };
  }, []);

  useEffect(() => {
    Router.setTemplate({ enabled: enable, width, x, y, url });
    Router.queueUpdate();
  }, [enable, width, x, y, url]);

  return (
    <templateContext.Provider
      value={{
+216 −0
Original line number Diff line number Diff line
import { PanZoom } from "@sc07-canvas/lib/src/renderer/PanZoom";
import { Canvas } from "./canvas";
import throttle from "lodash.throttle";
import EventEmitter from "eventemitter3";

const CLIENT_PARAMS = {
  canvas_x: "x",
  canvas_y: "y",
  canvas_zoom: "zoom",
  template_url: "tu",
  template_width: "tw",
  template_x: "tx",
  template_y: "ty",
};

export interface IRouterData {
  canvas?: {
    x: number;
    y: number;
    zoom?: number;
  };
  template?: {
    url: string;
    width?: number;
    x?: number;
    y?: number;
  };
}

interface RouterEvents {
  navigate(route: IRouterData): void;
}

class _Router extends EventEmitter<RouterEvents> {
  PanZoom: PanZoom | undefined;

  // React TemplateContext
  templateState: {
    enabled: boolean;
    width?: number;
    x: number;
    y: number;
    url?: string;
  } = {
    enabled: false,
    x: 0,
    y: 0,
  };

  constructor() {
    super();

    window.addEventListener("hashchange", this._hashChange.bind(this));
  }

  destroy() {
    // NOTE: this method *never* gets called because this is intended to be global
    window.removeEventListener("hashchange", this._hashChange.bind(this));
  }

  _hashChange(e: HashChangeEvent) {
    const data = this.get();
    console.info("[Router] Navigated", data);
    this.emit("navigate", data);
  }

  queueUpdate = throttle(this.update, 500);

  update() {
    const url = this.getURL();
    if (!url) return;

    console.log("[Router] Updating URL");
    window.history.replaceState({}, "", url);
  }

  getURL() {
    const canvas = Canvas.instance;
    // this is not that helpful because the data is more spread out
    // this gets replaced by using TemplateContext data
    // const template = Template.instance;

    if (!canvas) {
      console.warn("Router#update called but no canvas instance exists");
      return;
    }

    if (!this.PanZoom) {
      console.warn("Router#update called but no PanZoom instance exists");
    }

    const params = new URLSearchParams();

    const position = canvas.panZoomTransformToCanvas();
    params.set(CLIENT_PARAMS.canvas_x, position.canvasX + "");
    params.set(CLIENT_PARAMS.canvas_y, position.canvasY + "");
    params.set(
      CLIENT_PARAMS.canvas_zoom,
      (this.PanZoom!.transform.scale >> 0) + ""
    );

    if (this.templateState.enabled && this.templateState.url) {
      params.set(CLIENT_PARAMS.template_url, this.templateState.url + "");
      if (this.templateState.width)
        params.set(CLIENT_PARAMS.template_width, this.templateState.width + "");
      params.set(CLIENT_PARAMS.template_x, this.templateState.x + "");
      params.set(CLIENT_PARAMS.template_y, this.templateState.y + "");
    }

    return (
      window.location.protocol + "//" + window.location.host + "/#" + params
    );
  }

  /**
   * Parse the URL and return what was found, following specifications
   * There's no defaults, if it's not specified in the url, it's not specified in the return
   *
   * @returns
   */
  get(): IRouterData {
    const params = new URLSearchParams(window.location.hash.slice(1));

    let canvas:
      | {
          x: number;
          y: number;
          zoom?: number;
        }
      | undefined = undefined;

    if (
      params.has(CLIENT_PARAMS.canvas_x) &&
      params.has(CLIENT_PARAMS.canvas_y)
    ) {
      // params exist, now to validate
      // x & y or nothing; zoom is optional

      let x = parseInt(params.get(CLIENT_PARAMS.canvas_x) || "");
      let y = parseInt(params.get(CLIENT_PARAMS.canvas_y) || "");
      if (!isNaN(x) && !isNaN(y)) {
        // x & y are valid numbers
        canvas = {
          x,
          y,
        };

        if (params.has(CLIENT_PARAMS.canvas_zoom)) {
          let zoom = parseInt(params.get(CLIENT_PARAMS.canvas_zoom) || "");

          if (!isNaN(zoom)) {
            canvas.zoom = zoom;
          }
        }
      }
    }

    let template:
      | {
          url: string;
          width?: number;
          x?: number;
          y?: number;
        }
      | undefined = undefined;

    if (params.has(CLIENT_PARAMS.template_url)) {
      const url = params.get(CLIENT_PARAMS.template_url)!;
      template = { url };

      if (params.has(CLIENT_PARAMS.template_width)) {
        let width = parseInt(params.get(CLIENT_PARAMS.template_width) || "");

        if (!isNaN(width)) {
          template.width = width;
        }
      }

      if (
        params.has(CLIENT_PARAMS.template_x) &&
        params.has(CLIENT_PARAMS.template_y)
      ) {
        // both x & y has to be set

        let x = parseInt(params.get(CLIENT_PARAMS.template_x) || "");
        let y = parseInt(params.get(CLIENT_PARAMS.template_y) || "");

        if (!isNaN(x) && !isNaN(y)) {
          template.x = x;
          template.y = y;
        }
      }
    }

    return {
      canvas,
      template,
    };
  }

  /**
   * Accept updates to local copy of TemplateContext from React
   * @param args
   */
  setTemplate(args: {
    enabled: boolean;
    width?: number;
    x: number;
    y: number;
    url?: string;
  }) {
    this.templateState = args;
  }
}

export const Router = new _Router();

packages/client/src/lib/routes.ts

deleted100644 → 0
+0 −38
Original line number Diff line number Diff line
import { ICanvasPosition } from "@sc07-canvas/lib/src/net";

export interface ITemplateState {
  url: string;
  width: number;
  x: number;
  y: number;
  opacity: number;
}

export const Routes = {
  canvas: ({
    pos,
    template,
  }: {
    pos?: ICanvasPosition;
    template?: ITemplateState;
  }) => {
    const params = new URLSearchParams();

    if (pos) {
      params.set("x", pos.x + "");
      params.set("y", pos.y + "");
      params.set("zoom", pos.zoom + "");
    }

    if (template) {
      let { url, width, x, y, opacity } = template;
      params.set("template.url", url);
      params.set("template.width", width + "");
      params.set("template.x", x + "");
      params.set("template.y", y + "");
      params.set("template.opacity", opacity + "");
    }

    return "/#" + params;
  },
};