Graphite/frontend/src/dispatcher/js-messages.ts

360 lines
9.2 KiB
TypeScript

/* eslint-disable camelcase */
/* eslint-disable max-classes-per-file */
import { Transform, Type } from "class-transformer";
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
export class JsMessage {
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
static readonly jsMessageMarker = true;
}
// ============================================================================
// Add additional classes to replicate Rust's FrontendMessages and data structures below.
//
// Remember to add each message to the `messageConstructors` export at the bottom of the file.
//
// Read class-transformer docs at https://github.com/typestack/class-transformer#table-of-contents
// for details about how to transform the JSON from wasm-bindgen into classes.
// ============================================================================
// Allows the auto save system to use a string for the id rather than a BigInt.
// IndexedDb does not allow for BigInts as primary keys. TypeScript does not allow
// subclasses to change the type of class variables in subclasses. It is an abstract
// class to point out that it should not be instantiated directly.
export abstract class DocumentDetails {
readonly name!: string;
readonly is_saved!: boolean;
readonly id!: BigInt | string;
get displayName() {
return `${this.name}${this.is_saved ? "" : "*"}`;
}
}
export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: BigInt;
}
export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => FrontendDocumentDetails)
readonly open_documents!: FrontendDocumentDetails[];
}
export type HintData = HintInfo[][];
export class UpdateInputHints extends JsMessage {
@Type(() => HintInfo)
readonly hint_data!: HintData;
}
export type KeysGroup = string[];
export class HintInfo {
readonly keys!: string[];
readonly mouse!: KeysGroup | null;
readonly label!: string;
readonly plus!: boolean;
}
const To255Scale = Transform(({ value }) => value * 255);
export class Color {
@To255Scale
readonly red!: number;
@To255Scale
readonly green!: number;
@To255Scale
readonly blue!: number;
readonly alpha!: number;
toRgba() {
return { r: this.red, g: this.green, b: this.blue, a: this.alpha };
}
toRgbaCSS() {
const { r, g, b, a } = this.toRgba();
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
}
export class UpdateWorkingColors extends JsMessage {
@Type(() => Color)
readonly primary!: Color;
@Type(() => Color)
readonly secondary!: Color;
}
export class SetActiveTool extends JsMessage {
readonly tool_name!: string;
readonly tool_options!: object;
}
export class SetActiveDocument extends JsMessage {
readonly document_id!: BigInt;
}
export class DisplayError extends JsMessage {
readonly title!: string;
readonly description!: string;
}
export class DisplayPanic extends JsMessage {
readonly panic_info!: string;
readonly title!: string;
readonly description!: string;
}
export class DisplayConfirmationToCloseDocument extends JsMessage {
readonly document_id!: BigInt;
}
export class DisplayConfirmationToCloseAllDocuments extends JsMessage {}
export class DisplayAboutGraphiteDialog extends JsMessage {}
export class UpdateArtwork extends JsMessage {
readonly svg!: string;
}
export class UpdateOverlays extends JsMessage {
readonly svg!: string;
}
const TupleToVec2 = Transform(({ value }) => ({ x: value[0], y: value[1] }));
export class UpdateScrollbars extends JsMessage {
@TupleToVec2
readonly position!: { x: number; y: number };
@TupleToVec2
readonly size!: { x: number; y: number };
@TupleToVec2
readonly multiplier!: { x: number; y: number };
}
export class UpdateRulers extends JsMessage {
@TupleToVec2
readonly origin!: { x: number; y: number };
readonly spacing!: number;
readonly interval!: number;
}
export class ExportDocument extends JsMessage {
readonly document!: string;
readonly name!: string;
}
export class SaveDocument extends JsMessage {
readonly document!: string;
readonly name!: string;
}
export class OpenDocumentBrowse extends JsMessage {}
export class DocumentChanged extends JsMessage {}
export class DisplayFolderTreeStructure extends JsMessage {
constructor(readonly layerId: BigInt, readonly children: DisplayFolderTreeStructure[]) {
super();
}
}
interface DataBuffer {
pointer: BigInt;
length: BigInt;
}
export function newDisplayFolderTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayFolderTreeStructure {
const { pointer, length } = input.data_buffer;
const pointerNum = Number(pointer);
const lengthNum = Number(length);
const wasmMemoryBuffer = wasm.wasm_memory().buffer;
// Decode the folder structure encoding
const encoding = new DataView(wasmMemoryBuffer, pointerNum, lengthNum);
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
const structureSectionLength = Number(encoding.getBigUint64(0, true));
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointerNum + 8, structureSectionLength * 8);
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8);
let layersEncountered = 0;
let currentFolder = new DisplayFolderTreeStructure(BigInt(-1), []);
const currentFolderStack = [currentFolder];
for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
const msbMask = BigInt(1) << BigInt(63);
// Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
// Store child folders in the current folder (until we are interrupted by an indent)
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
layersEncountered += 1;
const childLayer = new DisplayFolderTreeStructure(layerId, []);
currentFolder.children.push(childLayer);
}
// Check the sign of the MSB, where a 1 is a negative (outward) indent
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
// Inward
if (subsequentDirectionOfDepthChange) {
currentFolderStack.push(currentFolder);
currentFolder = currentFolder.children[currentFolder.children.length - 1];
}
// Outward
else {
const popped = currentFolderStack.pop();
if (!popped) throw Error("Too many negative indents in the folder structure");
if (popped) currentFolder = popped;
}
}
return currentFolder;
}
export class UpdateLayer extends JsMessage {
@Type(() => LayerPanelEntry)
readonly data!: LayerPanelEntry;
}
export class SetCanvasZoom extends JsMessage {
readonly new_zoom!: number;
}
export class SetCanvasRotation extends JsMessage {
readonly new_radians!: number;
}
export type BlendMode =
| "Normal"
| "Multiply"
| "Darken"
| "ColorBurn"
| "Screen"
| "Lighten"
| "ColorDodge"
| "Overlay"
| "SoftLight"
| "HardLight"
| "Difference"
| "Exclusion"
| "Hue"
| "Saturation"
| "Color"
| "Luminosity";
export class LayerPanelEntry {
name!: string;
visible!: boolean;
blend_mode!: BlendMode;
// On the rust side opacity is out of 1 rather than 100
@Transform(({ value }) => value * 100)
opacity!: number;
layer_type!: LayerType;
@Transform(({ value }) => new BigUint64Array(value))
path!: BigUint64Array;
@Type(() => LayerMetadata)
layer_metadata!: LayerMetadata;
thumbnail!: string;
}
export class LayerMetadata {
expanded!: boolean;
selected!: boolean;
}
export const LayerTypeOptions = {
Folder: "Folder",
Shape: "Shape",
Circle: "Circle",
Rect: "Rect",
Line: "Line",
PolyLine: "PolyLine",
Ellipse: "Ellipse",
} as const;
export type LayerType = typeof LayerTypeOptions[keyof typeof LayerTypeOptions];
export class IndexedDbDocumentDetails extends DocumentDetails {
@Transform(({ value }: { value: BigInt }) => value.toString())
id!: string;
}
export class AutoSaveDocument extends JsMessage {
document!: string;
@Type(() => IndexedDbDocumentDetails)
details!: IndexedDbDocumentDetails;
}
export class RemoveAutoSaveDocument extends JsMessage {
// Use a string since IndexedDB can not use BigInts for keys
@Transform(({ value }: { value: BigInt }) => value.toString())
document_id!: string;
}
// Any is used since the type of the object should be known from the rust side
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageConstructors: Record<string, MessageMaker> = {
UpdateArtwork,
UpdateOverlays,
UpdateScrollbars,
UpdateRulers,
ExportDocument,
SaveDocument,
OpenDocumentBrowse,
DisplayFolderTreeStructure: newDisplayFolderTreeStructure,
UpdateLayer,
SetActiveTool,
SetActiveDocument,
UpdateOpenDocumentsList,
UpdateInputHints,
UpdateWorkingColors,
SetCanvasZoom,
SetCanvasRotation,
DisplayError,
DisplayPanic,
DisplayConfirmationToCloseDocument,
DisplayConfirmationToCloseAllDocuments,
DisplayAboutGraphiteDialog,
AutoSaveDocument,
RemoveAutoSaveDocument,
} as const;
export type JsMessageType = keyof typeof messageConstructors;