Frontend refactor to move response handler, key input handling, and more into new utilities folder (#260)

Part of #124
This commit is contained in:
Keavon Chambers
2021-07-14 14:31:09 -07:00
committed by GitHub
parent 9d34aa4867
commit 726152759f
14 changed files with 80 additions and 66 deletions
+79
View File
@@ -0,0 +1,79 @@
export interface RGB {
r: number;
g: number;
b: number;
a: number;
}
export interface HSV {
h: number;
s: number;
v: number;
a: number;
}
export function hsvToRgb(hsv: HSV): RGB {
let { h } = hsv;
const { s, v } = hsv;
h *= 6;
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = Math.round([v, q, p, p, t, v][mod]);
const g = Math.round([t, v, v, q, p, p][mod]);
const b = Math.round([p, p, t, v, v, q][mod]);
return { r, g, b, a: hsv.a };
}
export function rgbToHsv(rgb: RGB) {
const { r, g, b } = rgb;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const s = max === 0 ? 0 : d / max;
const v = max;
let h = 0;
if (max === min) {
h = 0;
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
}
h /= 6;
}
return { h, s, v, a: rgb.a };
}
export function rgbToDecimalRgb(rgb: RGB) {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
return { r, g, b, a: rgb.a };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isRGB(data: any): data is RGB {
if (typeof data !== "object" || data === null) return false;
return (
typeof data.r === "number" &&
!Number.isNaN(data.r) &&
typeof data.g === "number" &&
!Number.isNaN(data.g) &&
typeof data.b === "number" &&
!Number.isNaN(data.b) &&
typeof data.a === "number" &&
!Number.isNaN(data.a)
);
}
+45
View File
@@ -0,0 +1,45 @@
const wasm = import("@/../wasm/pkg");
export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {
// Don't redirect user input from text entry into HTML elements
const target = e.target as HTMLElement;
if (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable) return false;
// Don't redirect a fullscreen request
if (e.key.toLowerCase() === "f11") return false;
// Don't redirect a reload request
if (e.key.toLowerCase() === "f5") return false;
// Don't redirect debugging tools
if (e.key.toLowerCase() === "f12") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "c") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "i") return false;
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "j") return false;
// Redirect to the backend
return true;
}
export async function handleKeyDown(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const { on_key_down } = await wasm;
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
on_key_down(e.key, modifiers);
}
}
export async function handleKeyUp(e: KeyboardEvent) {
if (shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const { on_key_up } = await wasm;
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
on_key_up(e.key, modifiers);
}
}
export function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean): number {
// eslint-disable-next-line no-bitwise
return Number(control) | (Number(shift) << 1) | (Number(alt) << 2);
}
+3
View File
@@ -0,0 +1,3 @@
export function clamp(value: number, min = 0, max = 1) {
return Math.max(min, Math.min(value, max));
}
@@ -0,0 +1,4 @@
// This file is instantiated by wasm-bindgen in `client\web\wasm\src\lib.rs` and re-exports the `handleResponse` function to
// provide access to the global copy of `response-handler.ts` with its shared state, not an isolated duplicate with empty state
export { handleResponse } from "@/utilities/response-handler";
@@ -0,0 +1,256 @@
import { reactive } from "vue";
/* eslint-disable @typescript-eslint/no-explicit-any */
type ResponseCallback = (responseData: Response) => void;
type ResponseMap = {
[response: string]: ResponseCallback | undefined;
};
const state = reactive({
responseMap: {} as ResponseMap,
});
export enum ResponseType {
UpdateCanvas = "UpdateCanvas",
ExportDocument = "ExportDocument",
ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder",
SetActiveTool = "SetActiveTool",
SetActiveDocument = "SetActiveDocument",
NewDocument = "NewDocument",
CloseDocument = "CloseDocument",
UpdateWorkingColors = "UpdateWorkingColors",
PromptCloseConfirmationModal = "PromptCloseConfirmationModal",
SetCanvasZoom = "SetCanvasZoom",
SetRotation = "SetRotation",
}
export function registerResponseHandler(responseType: ResponseType, callback: ResponseCallback) {
state.responseMap[responseType] = callback;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function handleResponse(responseType: string, responseData: any) {
const callback = state.responseMap[responseType];
const data = parseResponse(responseType, responseData);
if (callback && data) {
callback(data);
} else if (data) {
console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`);
} else {
console.error(`Received a Response of type "${responseType}" but but was not able to parse the data.`);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseResponse(responseType: string, data: any): Response {
switch (responseType) {
case "DocumentChanged":
return newDocumentChanged(data.DocumentChanged);
case "CollapseFolder":
return newCollapseFolder(data.CollapseFolder);
case "ExpandFolder":
return newExpandFolder(data.ExpandFolder);
case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool);
case "SetActiveDocument":
return newSetActiveDocument(data.SetActiveDocument);
case "NewDocument":
return newNewDocument(data.NewDocument);
case "CloseDocument":
return newCloseDocument(data.CloseDocument);
case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas);
case "SetCanvasZoom":
return newSetZoom(data.SetCanvasZoom);
case "SetRotation":
return newSetRotation(data.SetRotation);
case "ExportDocument":
return newExportDocument(data.ExportDocument);
case "UpdateWorkingColors":
return newUpdateWorkingColors(data.UpdateWorkingColors);
case "PromptCloseConfirmationModal":
return {};
default:
throw new Error(`Unrecognized origin/responseType pair: ${origin}, '${responseType}'`);
}
}
export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetCanvasZoom | SetRotation;
export interface CloseDocument {
document_index: number;
}
function newCloseDocument(input: any): CloseDocument {
return { document_index: input.document_index };
}
export interface Color {
red: number;
green: number;
blue: number;
alpha: number;
}
function newColor(input: any): Color {
// TODO: Possibly change this in the Rust side to avoid any pitfalls
return { red: input.red * 255, green: input.green * 255, blue: input.blue * 255, alpha: input.alpha };
}
export interface UpdateWorkingColors {
primary: Color;
secondary: Color;
}
function newUpdateWorkingColors(input: any): UpdateWorkingColors {
return {
primary: newColor(input.primary),
secondary: newColor(input.secondary),
};
}
export interface SetActiveTool {
tool_name: string;
}
function newSetActiveTool(input: any): SetActiveTool {
return {
tool_name: input.tool_name,
};
}
export interface SetActiveDocument {
document_index: number;
}
function newSetActiveDocument(input: any): SetActiveDocument {
return {
document_index: input.document_index,
};
}
export interface NewDocument {
document_name: string;
}
function newNewDocument(input: any): NewDocument {
return {
document_name: input.document_name,
};
}
export interface UpdateCanvas {
document: string;
}
function newUpdateCanvas(input: any): UpdateCanvas {
return {
document: input.document,
};
}
export interface ExportDocument {
document: string;
}
function newExportDocument(input: any): UpdateCanvas {
return {
document: input.document,
};
}
export type DocumentChanged = {};
function newDocumentChanged(_: any): DocumentChanged {
return {};
}
export interface CollapseFolder {
path: BigUint64Array;
}
function newCollapseFolder(input: any): CollapseFolder {
return {
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
};
}
export interface ExpandFolder {
path: BigUint64Array;
children: Array<LayerPanelEntry>;
}
function newExpandFolder(input: any): ExpandFolder {
return {
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
children: input.children.map((child: any) => newLayerPanelEntry(child)),
};
}
export interface SetCanvasZoom {
new_zoom: number;
}
function newSetZoom(input: any): SetCanvasZoom {
return {
new_zoom: input.new_zoom,
};
}
export interface SetRotation {
new_radians: number;
}
function newSetRotation(input: any): SetRotation {
return {
new_radians: input.new_radians,
};
}
export interface LayerPanelEntry {
name: string;
visible: boolean;
layer_type: LayerType;
path: BigUint64Array;
layer_data: LayerData;
}
function newLayerPanelEntry(input: any): LayerPanelEntry {
return {
name: input.name,
visible: input.visible,
layer_type: newLayerType(input.layer_type),
layer_data: newLayerData(input.layer_data),
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),
};
}
export interface LayerData {
expanded: boolean;
selected: boolean;
}
function newLayerData(input: any): LayerData {
return {
expanded: input.expanded,
selected: input.selected,
};
}
export enum LayerType {
Folder = "Folder",
Shape = "Shape",
Circle = "Circle",
Rect = "Rect",
Line = "Line",
PolyLine = "PolyLine",
Ellipse = "Ellipse",
}
function newLayerType(input: any): LayerType {
switch (input) {
case "Folder":
return LayerType.Folder;
case "Shape":
return LayerType.Shape;
case "Circle":
return LayerType.Circle;
case "Rect":
return LayerType.Rect;
case "Line":
return LayerType.Line;
case "PolyLine":
return LayerType.PolyLine;
case "Ellipse":
return LayerType.Ellipse;
default:
throw Error(`Received invalid input as an enum variant for LayerType: ${input}`);
}
}