rewrite router (related #33)

This commit is contained in:
Grant 2024-05-25 22:36:02 -06:00
parent 634a69e79c
commit 94b4cdd3b8
4 changed files with 300 additions and 104 deletions

View File

@ -5,9 +5,9 @@ import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer";
import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext"; import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext";
import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom"; import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { Routes } from "../lib/routes"; import { IPosition } from "@sc07-canvas/lib/src/net";
import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net";
import { Template } from "./Template"; import { Template } from "./Template";
import { IRouterData, Router } from "../lib/router";
export const CanvasWrapper = () => { export const CanvasWrapper = () => {
// to prevent safari from blurring things, use the zoom css property // 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 CanvasInner = () => {
const canvasRef = createRef<HTMLCanvasElement>(); const canvasRef = createRef<HTMLCanvasElement>();
const { config, setCanvasPosition, setCursorPosition } = useAppContext(); const { config, setCanvasPosition, setCursorPosition } = useAppContext();
const PanZoom = useContext(RendererContext); const PanZoom = useContext(RendererContext);
useEffect(() => {
Router.PanZoom = PanZoom;
// @ts-ignore
window.TEST_Router = Router;
}, [PanZoom]);
useEffect(() => { useEffect(() => {
if (!config.canvas || !canvasRef.current) return; if (!config.canvas || !canvasRef.current) return;
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
const canvasInstance = new Canvas(config, canvas, PanZoom); const canvasInstance = new Canvas(config, canvas, PanZoom);
const initAt = Date.now();
{ const handleNavigate = (data: IRouterData) => {
// TODO: handle hash changes and move viewport if (data.canvas) {
// NOTE: this will need to be cancelled if handleViewportMove was executed recently const position = canvasInstance.canvasToPanZoomTransform(
data.canvas.x,
data.canvas.y
);
const position = parseHashParams(canvasInstance); PanZoom.setPosition(
if (position) { {
PanZoom.setPosition(position, { suppressEmit: true }); 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) => { // initial load
const pos = canvasInstance.panZoomTransformToCanvas(); const initialRouter = Router.get();
console.log(
"[CanvasWrapper] Initial router data, handling navigate",
initialRouter
);
handleNavigate(initialRouter);
const canvasPosition: ICanvasPosition = { const handleViewportMove = (state: ViewportMoveEvent) => {
x: pos.canvasX, if (Date.now() - initAt < 60 * 1000) {
y: pos.canvasY, console.debug(
zoom: state.scale >> 0, "[CanvasWrapper] handleViewportMove called soon after init",
}; Date.now() - initAt
);
}
setCanvasPosition(canvasPosition); Router.queueUpdate();
};
window.location.replace(Routes.canvas({ pos: canvasPosition }));
}, 1000);
const handleCursorPos = throttle((pos: IPosition) => { const handleCursorPos = throttle((pos: IPosition) => {
if ( if (
@ -111,11 +92,13 @@ const CanvasInner = () => {
PanZoom.addListener("viewportMove", handleViewportMove); PanZoom.addListener("viewportMove", handleViewportMove);
canvasInstance.on("cursorPos", handleCursorPos); canvasInstance.on("cursorPos", handleCursorPos);
Router.on("navigate", handleNavigate);
return () => { return () => {
canvasInstance.destroy(); canvasInstance.destroy();
PanZoom.removeListener("viewportMove", handleViewportMove); PanZoom.removeListener("viewportMove", handleViewportMove);
canvasInstance.off("cursorPos", handleCursorPos); canvasInstance.off("cursorPos", handleCursorPos);
Router.off("navigate", handleNavigate);
}; };
// ! do not include canvasRef, it causes infinite re-renders // ! do not include canvasRef, it causes infinite re-renders

View File

@ -1,4 +1,11 @@
import { PropsWithChildren, createContext, useContext, useState } from "react"; import {
PropsWithChildren,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { IRouterData, Router } from "../lib/router";
interface ITemplate { interface ITemplate {
/** /**
@ -35,13 +42,41 @@ const templateContext = createContext<ITemplate>({} as any);
export const useTemplateContext = () => useContext(templateContext); export const useTemplateContext = () => useContext(templateContext);
export const TemplateContext = ({ children }: PropsWithChildren) => { export const TemplateContext = ({ children }: PropsWithChildren) => {
const [enable, setEnable] = useState(false); const routerData = Router.get();
const [url, setURL] = useState<string>(); const [enable, setEnable] = useState(!!routerData.template?.url);
const [width, setWidth] = useState<number>(); const [url, setURL] = useState<string | undefined>(routerData.template?.url);
const [x, setX] = useState(0); const [width, setWidth] = useState<number | undefined>(
const [y, setY] = useState(0); routerData.template?.width
);
const [x, setX] = useState(routerData.template?.x || 0);
const [y, setY] = useState(routerData.template?.y || 0);
const [opacity, setOpacity] = useState(100); 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 ( return (
<templateContext.Provider <templateContext.Provider
value={{ value={{

View File

@ -0,0 +1,216 @@
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();

View File

@ -1,38 +0,0 @@
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;
},
};