rewrite router (related #33)
This commit is contained in:
parent
634a69e79c
commit
94b4cdd3b8
|
@ -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
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
|
@ -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();
|
|
@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
Loading…
Reference in New Issue