mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 11:28:13 +08:00
Frontend refactor to move response handler, key input handling, and more into new utilities folder (#260)
Part of #124
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user