|
|
|
|
|
|
|
import { Transform, Type, plainToClass } from "class-transformer"; |
|
|
|
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/utility-functions/icons"; |
|
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js"; |
|
|
|
export class JsMessage { |
|
|
|
static readonly jsMessageMarker = true; |
|
} |
|
|
|
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] })); |
|
const ImportsToVec2Array = Transform(({ obj: { imports } }: { obj: { imports: [FrontendGraphOutput, number, number][] } }) => |
|
imports.map(([outputMetadata, x, y]) => ({ outputMetadata, position: { x, y } })), |
|
); |
|
const ExportsToVec2Array = Transform(({ obj: { exports } }: { obj: { exports: [FrontendGraphInput, number, number][] } }) => |
|
exports.map(([inputMetadata, x, y]) => ({ inputMetadata, position: { x, y } })), |
|
); |
|
|
|
|
|
|
|
export type XY = { x: number; y: number }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class UpdateBox extends JsMessage { |
|
readonly box!: Box | undefined; |
|
} |
|
|
|
export class UpdateClickTargets extends JsMessage { |
|
readonly clickTargets!: FrontendClickTargets | undefined; |
|
} |
|
|
|
const ContextTupleToVec2 = Transform((data) => { |
|
if (data.obj.contextMenuInformation === undefined) return undefined; |
|
const contextMenuCoordinates = { x: data.obj.contextMenuInformation.contextMenuCoordinates[0], y: data.obj.contextMenuInformation.contextMenuCoordinates[1] }; |
|
let contextMenuData = data.obj.contextMenuInformation.contextMenuData; |
|
if (contextMenuData.ToggleLayer !== undefined) { |
|
contextMenuData = { nodeId: contextMenuData.ToggleLayer.nodeId, currentlyIsNode: contextMenuData.ToggleLayer.currentlyIsNode }; |
|
} else if (contextMenuData.CreateNode !== undefined) { |
|
contextMenuData = { type: "CreateNode", compatibleType: contextMenuData.CreateNode.compatibleType }; |
|
} |
|
return { contextMenuCoordinates, contextMenuData }; |
|
}); |
|
|
|
export class UpdateContextMenuInformation extends JsMessage { |
|
@ContextTupleToVec2 |
|
readonly contextMenuInformation!: ContextMenuInformation | undefined; |
|
} |
|
|
|
export class UpdateImportsExports extends JsMessage { |
|
@ImportsToVec2Array |
|
readonly imports!: { outputMetadata: FrontendGraphOutput; position: XY }[]; |
|
|
|
@ExportsToVec2Array |
|
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[]; |
|
|
|
@TupleToVec2 |
|
readonly addImport!: XY | undefined; |
|
|
|
@TupleToVec2 |
|
readonly addExport!: XY | undefined; |
|
} |
|
|
|
export class UpdateInSelectedNetwork extends JsMessage { |
|
readonly inSelectedNetwork!: boolean; |
|
} |
|
|
|
export class UpdateImportReorderIndex extends JsMessage { |
|
readonly importIndex!: number | undefined; |
|
} |
|
|
|
export class UpdateExportReorderIndex extends JsMessage { |
|
readonly exportIndex!: number | undefined; |
|
} |
|
|
|
const LayerWidths = Transform(({ obj }) => obj.layerWidths); |
|
const ChainWidths = Transform(({ obj }) => obj.chainWidths); |
|
const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire); |
|
|
|
export class UpdateLayerWidths extends JsMessage { |
|
@LayerWidths |
|
readonly layerWidths!: Map<bigint, number>; |
|
@ChainWidths |
|
readonly chainWidths!: Map<bigint, number>; |
|
@HasLeftInputWire |
|
readonly hasLeftInputWire!: Map<bigint, boolean>; |
|
} |
|
|
|
export class UpdateNodeGraphNodes extends JsMessage { |
|
@Type(() => FrontendNode) |
|
readonly nodes!: FrontendNode[]; |
|
} |
|
|
|
export class UpdateVisibleNodes extends JsMessage { |
|
readonly nodes!: bigint[]; |
|
} |
|
|
|
export class UpdateNodeGraphWires extends JsMessage { |
|
readonly wires!: WireUpdate[]; |
|
} |
|
|
|
export class ClearAllNodeGraphWires extends JsMessage {} |
|
|
|
export class UpdateNodeGraphTransform extends JsMessage { |
|
readonly transform!: NodeGraphTransform; |
|
} |
|
|
|
const NodeDescriptions = Transform(({ obj }) => new Map(obj.nodeDescriptions)); |
|
|
|
export class SendUIMetadata extends JsMessage { |
|
@NodeDescriptions |
|
readonly nodeDescriptions!: Map<string, string>; |
|
@Type(() => FrontendNode) |
|
readonly nodeTypes!: FrontendNodeType[]; |
|
} |
|
|
|
export class UpdateNodeThumbnail extends JsMessage { |
|
readonly id!: bigint; |
|
|
|
readonly value!: string; |
|
} |
|
|
|
export class UpdateNodeGraphSelection extends JsMessage { |
|
@Type(() => BigInt) |
|
readonly selected!: bigint[]; |
|
} |
|
|
|
export class UpdateOpenDocumentsList extends JsMessage { |
|
@Type(() => FrontendDocumentDetails) |
|
readonly openDocuments!: FrontendDocumentDetails[]; |
|
} |
|
|
|
export class UpdateWirePathInProgress extends JsMessage { |
|
readonly wirePath!: WirePath | undefined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export abstract class DocumentDetails { |
|
readonly name!: string; |
|
|
|
readonly isAutoSaved!: boolean; |
|
|
|
readonly isSaved!: boolean; |
|
|
|
|
|
|
|
|
|
get displayName(): string { |
|
return `${this.name}${this.isSaved ? "" : "*"}`; |
|
} |
|
} |
|
|
|
export class FrontendDocumentDetails extends DocumentDetails { |
|
readonly id!: bigint; |
|
} |
|
|
|
export class Box { |
|
readonly startX!: number; |
|
|
|
readonly startY!: number; |
|
|
|
readonly endX!: number; |
|
|
|
readonly endY!: number; |
|
} |
|
|
|
export type FrontendClickTargets = { |
|
readonly nodeClickTargets: string[]; |
|
readonly layerClickTargets: string[]; |
|
readonly portClickTargets: string[]; |
|
readonly iconClickTargets: string[]; |
|
readonly allNodesBoundingBox: string; |
|
readonly importExportsBoundingBox: string; |
|
readonly modifyImportExport: string[]; |
|
}; |
|
|
|
export type ContextMenuInformation = { |
|
contextMenuCoordinates: XY; |
|
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean }; |
|
}; |
|
|
|
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Group" | "Artboard"; |
|
|
|
export class Node { |
|
readonly index!: bigint; |
|
|
|
readonly nodeId?: bigint; |
|
} |
|
|
|
const CreateOutputConnectorOptional = Transform(({ obj }) => { |
|
if (obj.connectedTo == undefined) { |
|
return undefined; |
|
} |
|
if (obj.connectedTo?.export !== undefined) { |
|
return { index: obj.connectedTo?.export }; |
|
} else if (obj.connectedTo?.import !== undefined) { |
|
return { index: obj.connectedTo?.import }; |
|
} else { |
|
if (obj.connectedTo?.node.inputIndex !== undefined) { |
|
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.inputIndex }; |
|
} else { |
|
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.outputIndex }; |
|
} |
|
} |
|
}); |
|
|
|
export class FrontendGraphInput { |
|
readonly dataType!: FrontendGraphDataType; |
|
|
|
readonly name!: string; |
|
|
|
readonly description!: string; |
|
|
|
readonly resolvedType!: string; |
|
|
|
readonly validTypes!: string[]; |
|
|
|
@CreateOutputConnectorOptional |
|
connectedTo!: Node | undefined; |
|
} |
|
|
|
const CreateInputConnectorArray = Transform(({ obj }) => { |
|
const newInputConnectors: Node[] = []; |
|
obj.connectedTo.forEach((connector: any) => { |
|
if (connector.export !== undefined) { |
|
newInputConnectors.push({ index: connector.export }); |
|
} else if (connector.import !== undefined) { |
|
newInputConnectors.push({ index: connector.import }); |
|
} else { |
|
if (connector.node.inputIndex !== undefined) { |
|
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.inputIndex }); |
|
} else { |
|
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.outputIndex }); |
|
} |
|
} |
|
}); |
|
return newInputConnectors; |
|
}); |
|
|
|
export class FrontendGraphOutput { |
|
readonly dataType!: FrontendGraphDataType; |
|
|
|
readonly name!: string; |
|
|
|
readonly description!: string; |
|
|
|
readonly resolvedType!: string; |
|
|
|
@CreateInputConnectorArray |
|
connectedTo!: Node[]; |
|
} |
|
|
|
export class FrontendNode { |
|
readonly isLayer!: boolean; |
|
|
|
readonly canBeLayer!: boolean; |
|
|
|
readonly id!: bigint; |
|
|
|
readonly reference!: string | undefined; |
|
|
|
readonly displayName!: string; |
|
|
|
@Type(() => FrontendGraphInput) |
|
readonly primaryInput!: FrontendGraphInput | undefined; |
|
|
|
@Type(() => FrontendGraphInput) |
|
readonly exposedInputs!: FrontendGraphInput[]; |
|
|
|
@Type(() => FrontendGraphOutput) |
|
readonly primaryOutput!: FrontendGraphOutput | undefined; |
|
|
|
@Type(() => FrontendGraphOutput) |
|
readonly exposedOutputs!: FrontendGraphOutput[]; |
|
|
|
@TupleToVec2 |
|
readonly position!: XY | undefined; |
|
|
|
|
|
|
|
readonly previewed!: boolean; |
|
|
|
readonly visible!: boolean; |
|
|
|
readonly unlocked!: boolean; |
|
|
|
readonly errors!: string | undefined; |
|
|
|
readonly uiOnly!: boolean; |
|
} |
|
|
|
export class FrontendNodeType { |
|
readonly name!: string; |
|
|
|
readonly category!: string; |
|
|
|
readonly inputTypes!: string[]; |
|
} |
|
|
|
export class NodeGraphTransform { |
|
readonly scale!: number; |
|
readonly x!: number; |
|
readonly y!: number; |
|
} |
|
|
|
export class WirePath { |
|
readonly pathString!: string; |
|
readonly dataType!: FrontendGraphDataType; |
|
readonly thick!: boolean; |
|
readonly dashed!: boolean; |
|
} |
|
|
|
export class WireUpdate { |
|
readonly id!: bigint; |
|
readonly inputIndex!: number; |
|
readonly wirePathUpdate!: WirePath | undefined; |
|
} |
|
|
|
export class IndexedDbDocumentDetails extends DocumentDetails { |
|
@Transform(({ value }: { value: bigint }) => value.toString()) |
|
id!: string; |
|
} |
|
|
|
export class TriggerIndexedDbWriteDocument extends JsMessage { |
|
document!: string; |
|
|
|
@Type(() => IndexedDbDocumentDetails) |
|
details!: IndexedDbDocumentDetails; |
|
|
|
version!: string; |
|
} |
|
|
|
export class TriggerIndexedDbRemoveDocument extends JsMessage { |
|
|
|
@Transform(({ value }: { value: bigint }) => value.toString()) |
|
documentId!: string; |
|
} |
|
|
|
export class UpdateInputHints extends JsMessage { |
|
@Type(() => HintInfo) |
|
readonly hintData!: HintData; |
|
} |
|
|
|
export type HintData = HintGroup[]; |
|
|
|
export type HintGroup = HintInfo[]; |
|
|
|
export class HintInfo { |
|
readonly keyGroups!: LayoutKeysGroup[]; |
|
|
|
readonly keyGroupsMac!: LayoutKeysGroup[] | undefined; |
|
|
|
readonly mouse!: MouseMotion | undefined; |
|
|
|
readonly label!: string; |
|
|
|
readonly plus!: boolean; |
|
|
|
readonly slash!: boolean; |
|
} |
|
|
|
|
|
export type KeyRaw = string; |
|
|
|
export type Key = { key: KeyRaw; label: string }; |
|
export type LayoutKeysGroup = Key[]; |
|
export type ActionKeys = { keys: LayoutKeysGroup }; |
|
|
|
export type MouseMotion = string; |
|
|
|
|
|
export type HSVA = { h: number; s: number; v: number; a: number }; |
|
export type HSV = { h: number; s: number; v: number }; |
|
export type RGBA = { r: number; g: number; b: number; a: number }; |
|
export type RGB = { r: number; g: number; b: number }; |
|
|
|
export class Gradient { |
|
readonly stops!: { position: number; color: Color }[]; |
|
|
|
constructor(stops: { position: number; color: Color }[]) { |
|
this.stops = stops; |
|
} |
|
|
|
toLinearGradientCSS(): string { |
|
if (this.stops.length === 1) { |
|
return `linear-gradient(to right, ${this.stops[0].color.toHexOptionalAlpha()} 0%, ${this.stops[0].color.toHexOptionalAlpha()} 100%)`; |
|
} |
|
const pieces = this.stops.map((stop) => `${stop.color.toHexOptionalAlpha()} ${stop.position * 100}%`); |
|
return `linear-gradient(to right, ${pieces.join(", ")})`; |
|
} |
|
|
|
toLinearGradientCSSNoAlpha(): string { |
|
if (this.stops.length === 1) { |
|
return `linear-gradient(to right, ${this.stops[0].color.toHexNoAlpha()} 0%, ${this.stops[0].color.toHexNoAlpha()} 100%)`; |
|
} |
|
const pieces = this.stops.map((stop) => `${stop.color.toHexNoAlpha()} ${stop.position * 100}%`); |
|
return `linear-gradient(to right, ${pieces.join(", ")})`; |
|
} |
|
|
|
firstColor(): Color | undefined { |
|
return this.stops[0]?.color; |
|
} |
|
|
|
lastColor(): Color | undefined { |
|
return this.stops[this.stops.length - 1]?.color; |
|
} |
|
|
|
atIndex(index: number): { position: number; color: Color } | undefined { |
|
return this.stops[index]; |
|
} |
|
|
|
colorAtIndex(index: number): Color | undefined { |
|
return this.stops[index]?.color; |
|
} |
|
|
|
positionAtIndex(index: number): number | undefined { |
|
return this.stops[index]?.position; |
|
} |
|
} |
|
|
|
|
|
export class Color { |
|
readonly red!: number; |
|
|
|
readonly green!: number; |
|
|
|
readonly blue!: number; |
|
|
|
readonly alpha!: number; |
|
|
|
readonly none!: boolean; |
|
|
|
constructor(); |
|
|
|
constructor(none: "none"); |
|
|
|
constructor(hsva: HSVA); |
|
|
|
constructor(red: number, green: number, blue: number, alpha: number); |
|
|
|
constructor(firstArg?: "none" | HSVA | number, green?: number, blue?: number, alpha?: number) { |
|
|
|
if (firstArg === undefined) { |
|
this.red = 0; |
|
this.green = 0; |
|
this.blue = 0; |
|
this.alpha = 1; |
|
this.none = false; |
|
} else if (firstArg === "none") { |
|
this.red = 0; |
|
this.green = 0; |
|
this.blue = 0; |
|
this.alpha = 1; |
|
this.none = true; |
|
} |
|
|
|
else if (typeof firstArg === "object" && green === undefined && blue === undefined && alpha === undefined) { |
|
const { h, s, v } = firstArg; |
|
const convert = (n: number): number => { |
|
const k = (n + h * 6) % 6; |
|
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0); |
|
}; |
|
|
|
this.red = convert(5); |
|
this.green = convert(3); |
|
this.blue = convert(1); |
|
this.alpha = firstArg.a; |
|
this.none = false; |
|
} |
|
|
|
else if (typeof firstArg === "number" && typeof green === "number" && typeof blue === "number" && typeof alpha === "number") { |
|
this.red = firstArg; |
|
this.green = green; |
|
this.blue = blue; |
|
this.alpha = alpha; |
|
this.none = false; |
|
} |
|
} |
|
|
|
static fromCSS(colorCode: string): Color | undefined { |
|
|
|
let colorValue = colorCode.trim(); |
|
if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) { |
|
const digit = colorValue.charAt(1); |
|
colorValue = `#${digit}${digit}${digit}`; |
|
} |
|
|
|
const canvas = document.createElement("canvas"); |
|
canvas.width = 1; |
|
canvas.height = 1; |
|
const context = canvas.getContext("2d"); |
|
if (!context) return undefined; |
|
|
|
context.clearRect(0, 0, 1, 1); |
|
|
|
context.fillStyle = "black"; |
|
context.fillStyle = colorValue; |
|
const comparisonA = context.fillStyle; |
|
|
|
context.fillStyle = "white"; |
|
context.fillStyle = colorValue; |
|
const comparisonB = context.fillStyle; |
|
|
|
|
|
if (comparisonA !== comparisonB) { |
|
|
|
if (colorValue.trim().charAt(0) !== "#") return Color.fromCSS(`#${colorValue.trim()}`); |
|
return undefined; |
|
} |
|
|
|
context.fillRect(0, 0, 1, 1); |
|
|
|
const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data]; |
|
return new Color(r / 255, g / 255, b / 255, a / 255); |
|
} |
|
|
|
equals(other: Color): boolean { |
|
if (this.none && other.none) return true; |
|
return Math.abs(this.red - other.red) < 1e-6 && Math.abs(this.green - other.green) < 1e-6 && Math.abs(this.blue - other.blue) < 1e-6 && Math.abs(this.alpha - other.alpha) < 1e-6; |
|
} |
|
|
|
lerp(other: Color, t: number): Color { |
|
return new Color(this.red * (1 - t) + other.red * t, this.green * (1 - t) + other.green * t, this.blue * (1 - t) + other.blue * t, this.alpha * (1 - t) + other.alpha * t); |
|
} |
|
|
|
toHexNoAlpha(): string | undefined { |
|
if (this.none) return undefined; |
|
|
|
const r = Math.round(this.red * 255) |
|
.toString(16) |
|
.padStart(2, "0"); |
|
const g = Math.round(this.green * 255) |
|
.toString(16) |
|
.padStart(2, "0"); |
|
const b = Math.round(this.blue * 255) |
|
.toString(16) |
|
.padStart(2, "0"); |
|
|
|
return `#${r}${g}${b}`; |
|
} |
|
|
|
toHexOptionalAlpha(): string | undefined { |
|
if (this.none) return undefined; |
|
|
|
const hex = this.toHexNoAlpha(); |
|
const a = Math.round(this.alpha * 255) |
|
.toString(16) |
|
.padStart(2, "0"); |
|
|
|
return a === "ff" ? hex : `${hex}${a}`; |
|
} |
|
|
|
toRgb255(): RGB | undefined { |
|
if (this.none) return undefined; |
|
|
|
return { |
|
r: Math.round(this.red * 255), |
|
g: Math.round(this.green * 255), |
|
b: Math.round(this.blue * 255), |
|
}; |
|
} |
|
|
|
toRgbCSS(): string | undefined { |
|
const rgb = this.toRgb255(); |
|
if (!rgb) return undefined; |
|
|
|
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; |
|
} |
|
|
|
toRgbaCSS(): string | undefined { |
|
const rgb = this.toRgb255(); |
|
if (!rgb) return undefined; |
|
|
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${this.alpha})`; |
|
} |
|
|
|
toHSV(): HSV | undefined { |
|
const hsva = this.toHSVA(); |
|
if (!hsva) return undefined; |
|
|
|
return { h: hsva.h, s: hsva.s, v: hsva.v }; |
|
} |
|
|
|
toHSVA(): HSVA | undefined { |
|
if (this.none) return undefined; |
|
|
|
const { red: r, green: g, blue: b, alpha: a } = this; |
|
|
|
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) { |
|
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 }; |
|
} |
|
|
|
toHsvDegreesAndPercent(): HSV | undefined { |
|
const hsva = this.toHSVA(); |
|
if (!hsva) return undefined; |
|
|
|
return { h: hsva.h * 360, s: hsva.s * 100, v: hsva.v * 100 }; |
|
} |
|
|
|
toHsvaDegreesAndPercent(): HSVA | undefined { |
|
const hsva = this.toHSVA(); |
|
if (!hsva) return undefined; |
|
|
|
return { h: hsva.h * 360, s: hsva.s * 100, v: hsva.v * 100, a: hsva.a * 100 }; |
|
} |
|
|
|
opaque(): Color | undefined { |
|
if (this.none) return undefined; |
|
|
|
return new Color(this.red, this.green, this.blue, 1); |
|
} |
|
|
|
luminance(): number | undefined { |
|
if (this.none) return undefined; |
|
|
|
|
|
const r = this.red * this.alpha + (1 - this.alpha); |
|
const g = this.green * this.alpha + (1 - this.alpha); |
|
const b = this.blue * this.alpha + (1 - this.alpha); |
|
|
|
|
|
|
|
const linearR = r <= 0.04045 ? r / 12.92 : ((r + 0.055) / 1.055) ** 2.4; |
|
const linearG = g <= 0.04045 ? g / 12.92 : ((g + 0.055) / 1.055) ** 2.4; |
|
const linearB = b <= 0.04045 ? b / 12.92 : ((b + 0.055) / 1.055) ** 2.4; |
|
|
|
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722; |
|
} |
|
|
|
contrastingColor(): "black" | "white" { |
|
if (this.none) return "black"; |
|
|
|
const luminance = this.luminance(); |
|
|
|
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white"; |
|
} |
|
} |
|
|
|
export class UpdateActiveDocument extends JsMessage { |
|
readonly documentId!: bigint; |
|
} |
|
|
|
export class DisplayDialogPanic extends JsMessage { |
|
readonly panicInfo!: string; |
|
} |
|
|
|
export class DisplayDialog extends JsMessage { |
|
readonly title!: string; |
|
readonly icon!: IconName; |
|
} |
|
|
|
export class UpdateDocumentArtwork extends JsMessage { |
|
readonly svg!: string; |
|
} |
|
|
|
export class UpdateDocumentScrollbars extends JsMessage { |
|
@TupleToVec2 |
|
readonly position!: XY; |
|
|
|
@TupleToVec2 |
|
readonly size!: XY; |
|
|
|
@TupleToVec2 |
|
readonly multiplier!: XY; |
|
} |
|
|
|
export class UpdateDocumentRulers extends JsMessage { |
|
@TupleToVec2 |
|
readonly origin!: XY; |
|
|
|
readonly spacing!: number; |
|
|
|
readonly interval!: number; |
|
|
|
readonly visible!: boolean; |
|
} |
|
|
|
export class UpdateEyedropperSamplingState extends JsMessage { |
|
@TupleToVec2 |
|
readonly mousePosition!: XY | undefined; |
|
|
|
readonly primaryColor!: string; |
|
|
|
readonly secondaryColor!: string; |
|
|
|
readonly setColorChoice!: "Primary" | "Secondary" | undefined; |
|
} |
|
|
|
const mouseCursorIconCSSNames = { |
|
Default: "default", |
|
None: "none", |
|
ZoomIn: "zoom-in", |
|
ZoomOut: "zoom-out", |
|
Grabbing: "grabbing", |
|
Crosshair: "crosshair", |
|
Text: "text", |
|
Move: "move", |
|
NSResize: "ns-resize", |
|
EWResize: "ew-resize", |
|
NESWResize: "nesw-resize", |
|
NWSEResize: "nwse-resize", |
|
Rotate: "custom-rotate", |
|
} as const; |
|
export type MouseCursor = keyof typeof mouseCursorIconCSSNames; |
|
export type MouseCursorIcon = (typeof mouseCursorIconCSSNames)[MouseCursor]; |
|
|
|
export class UpdateGraphViewOverlay extends JsMessage { |
|
open!: boolean; |
|
} |
|
|
|
export class UpdateGraphFadeArtwork extends JsMessage { |
|
readonly percentage!: number; |
|
} |
|
|
|
export class UpdateSpreadsheetState extends JsMessage { |
|
readonly open!: boolean; |
|
|
|
readonly node!: bigint | undefined; |
|
} |
|
|
|
export class UpdateMouseCursor extends JsMessage { |
|
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias") |
|
readonly cursor!: MouseCursorIcon; |
|
} |
|
|
|
export class TriggerLoadFirstAutoSaveDocument extends JsMessage {} |
|
export class TriggerLoadRestAutoSaveDocuments extends JsMessage {} |
|
|
|
export class TriggerLoadPreferences extends JsMessage {} |
|
|
|
export class TriggerFetchAndOpenDocument extends JsMessage { |
|
readonly name!: string; |
|
|
|
readonly filename!: string; |
|
} |
|
|
|
export class TriggerOpenDocument extends JsMessage {} |
|
|
|
export class TriggerImport extends JsMessage {} |
|
|
|
export class TriggerPaste extends JsMessage {} |
|
|
|
export class TriggerDelayedZoomCanvasToFitAll extends JsMessage {} |
|
|
|
export class TriggerDownloadImage extends JsMessage { |
|
readonly svg!: string; |
|
|
|
readonly name!: string; |
|
|
|
readonly mime!: string; |
|
|
|
@TupleToVec2 |
|
readonly size!: XY; |
|
} |
|
|
|
export class TriggerDownloadTextFile extends JsMessage { |
|
readonly document!: string; |
|
|
|
readonly name!: string; |
|
} |
|
|
|
export class TriggerSavePreferences extends JsMessage { |
|
readonly preferences!: Record<string, unknown>; |
|
} |
|
|
|
export class TriggerSaveActiveDocument extends JsMessage { |
|
readonly documentId!: bigint; |
|
} |
|
|
|
export class DocumentChanged extends JsMessage {} |
|
|
|
export type DataBuffer = { |
|
pointer: bigint; |
|
length: bigint; |
|
}; |
|
|
|
export class UpdateDocumentLayerStructureJs extends JsMessage { |
|
readonly dataBuffer!: DataBuffer; |
|
} |
|
|
|
export class DisplayEditableTextbox extends JsMessage { |
|
readonly text!: string; |
|
|
|
readonly lineHeightRatio!: number; |
|
|
|
readonly fontSize!: number; |
|
|
|
@Type(() => Color) |
|
readonly color!: Color; |
|
|
|
readonly url!: string; |
|
|
|
readonly transform!: number[]; |
|
|
|
readonly maxWidth!: undefined | number; |
|
|
|
readonly maxHeight!: undefined | number; |
|
} |
|
|
|
export class DisplayEditableTextboxTransform extends JsMessage { |
|
readonly transform!: number[]; |
|
} |
|
|
|
export class DisplayRemoveEditableTextbox extends JsMessage {} |
|
|
|
export class UpdateDocumentLayerDetails extends JsMessage { |
|
@Type(() => LayerPanelEntry) |
|
readonly data!: LayerPanelEntry; |
|
} |
|
|
|
export class LayerPanelEntry { |
|
id!: bigint; |
|
|
|
name!: string; |
|
|
|
alias!: string; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
inSelectedNetwork!: boolean; |
|
|
|
childrenAllowed!: boolean; |
|
|
|
childrenPresent!: boolean; |
|
|
|
expanded!: boolean; |
|
|
|
@Transform(({ value }: { value: bigint }) => Number(value)) |
|
depth!: number; |
|
|
|
visible!: boolean; |
|
|
|
parentsVisible!: boolean; |
|
|
|
unlocked!: boolean; |
|
|
|
parentsUnlocked!: boolean; |
|
|
|
parentId!: bigint | undefined; |
|
|
|
selected!: boolean; |
|
|
|
ancestorOfSelected!: boolean; |
|
|
|
descendantOfSelected!: boolean; |
|
|
|
clipped!: boolean; |
|
|
|
clippable!: boolean; |
|
} |
|
|
|
export class DisplayDialogDismiss extends JsMessage {} |
|
|
|
export class Font { |
|
fontFamily!: string; |
|
|
|
fontStyle!: string; |
|
} |
|
|
|
export class TriggerFontLoad extends JsMessage { |
|
@Type(() => Font) |
|
font!: Font; |
|
} |
|
|
|
export class TriggerVisitLink extends JsMessage { |
|
url!: string; |
|
} |
|
|
|
export class TriggerTextCommit extends JsMessage {} |
|
|
|
export class TriggerTextCopy extends JsMessage { |
|
readonly copyText!: string; |
|
} |
|
|
|
export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage { |
|
readonly commitDate!: string; |
|
} |
|
|
|
|
|
|
|
export abstract class WidgetProps { |
|
kind!: WidgetPropsNames; |
|
} |
|
|
|
export class CheckboxInput extends WidgetProps { |
|
checked!: boolean; |
|
|
|
disabled!: boolean; |
|
|
|
icon!: IconName; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
forLabel!: bigint | undefined; |
|
} |
|
|
|
export class ColorInput extends WidgetProps { |
|
@Transform(({ value }) => { |
|
if (value instanceof Gradient) return value; |
|
const gradient = value["Gradient"]; |
|
if (gradient) { |
|
const stops = gradient.map(([position, color]: [number, color: { red: number; green: number; blue: number; alpha: number }]) => ({ |
|
position, |
|
color: new Color(color.red, color.green, color.blue, color.alpha), |
|
})); |
|
return new Gradient(stops); |
|
} |
|
|
|
if (value instanceof Color) return value; |
|
const solid = value["Solid"]; |
|
if (solid) { |
|
return new Color(solid.red, solid.green, solid.blue, solid.alpha); |
|
} |
|
|
|
return new Color("none"); |
|
}) |
|
value!: FillChoice; |
|
|
|
disabled!: boolean; |
|
|
|
allowNone!: boolean; |
|
|
|
|
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export type FillChoice = Color | Gradient; |
|
|
|
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number { |
|
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor]; |
|
const [range1, range2] = pair.map((color) => Color.fromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || new Color("none")); |
|
|
|
const contrast = (color: Color): number => { |
|
const colorLuminance = color.luminance() || 0; |
|
let rangeLuminance1 = range1.luminance() || 0; |
|
let rangeLuminance2 = range2.luminance() || 0; |
|
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)]; |
|
|
|
const distance = (() => { |
|
if (colorLuminance < rangeLuminance1) return rangeLuminance1 - colorLuminance; |
|
if (colorLuminance > rangeLuminance2) return colorLuminance - rangeLuminance2; |
|
return 0; |
|
})(); |
|
|
|
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (color.toHSV()?.s || 0)); |
|
}; |
|
|
|
if (value instanceof Gradient) { |
|
if (value.stops.length === 0) return 0; |
|
|
|
const first = contrast(value.stops[0].color); |
|
const last = contrast(value.stops[value.stops.length - 1].color); |
|
|
|
return Math.min(first, last); |
|
} |
|
|
|
return contrast(value); |
|
} |
|
|
|
type MenuEntryCommon = { |
|
label: string; |
|
icon?: IconName; |
|
shortcut?: ActionKeys; |
|
}; |
|
|
|
|
|
export type MenuBarEntry = MenuEntryCommon & { |
|
action: Widget; |
|
children?: MenuBarEntry[][]; |
|
disabled?: boolean; |
|
}; |
|
|
|
|
|
export type MenuListEntry = MenuEntryCommon & { |
|
action?: () => void; |
|
children?: MenuListEntry[][]; |
|
|
|
value: string; |
|
shortcutRequiresLock?: boolean; |
|
disabled?: boolean; |
|
tooltip?: string; |
|
font?: URL; |
|
}; |
|
|
|
export class CurveManipulatorGroup { |
|
anchor!: [number, number]; |
|
handles!: [[number, number], [number, number]]; |
|
} |
|
|
|
export class Curve { |
|
manipulatorGroups!: CurveManipulatorGroup[]; |
|
firstHandle!: [number, number]; |
|
lastHandle!: [number, number]; |
|
} |
|
|
|
export class CurveInput extends WidgetProps { |
|
value!: Curve; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class DropdownInput extends WidgetProps { |
|
entries!: MenuListEntry[][]; |
|
|
|
selectedIndex!: number | undefined; |
|
|
|
drawIcon!: boolean; |
|
|
|
interactive!: boolean; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
|
|
|
|
minWidth!: number; |
|
|
|
maxWidth!: number; |
|
} |
|
|
|
export class FontInput extends WidgetProps { |
|
fontFamily!: string; |
|
|
|
fontStyle!: string; |
|
|
|
isStyle!: boolean; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class IconButton extends WidgetProps { |
|
icon!: IconName; |
|
|
|
hoverIcon!: IconName | undefined; |
|
|
|
size!: IconSize; |
|
|
|
disabled!: boolean; |
|
|
|
active!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class IconLabel extends WidgetProps { |
|
icon!: IconName; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class ImageButton extends WidgetProps { |
|
image!: IconName; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
width!: string | undefined; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
height!: string | undefined; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None"; |
|
export type NumberInputMode = "Increment" | "Range"; |
|
|
|
export class NumberInput extends WidgetProps { |
|
|
|
|
|
label!: string | undefined; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
|
|
|
|
disabled!: boolean; |
|
|
|
|
|
|
|
value!: number | undefined; |
|
|
|
min!: number | undefined; |
|
|
|
max!: number | undefined; |
|
|
|
isInteger!: boolean; |
|
|
|
|
|
|
|
displayDecimalPlaces!: number; |
|
|
|
unit!: string; |
|
|
|
unitIsHiddenWhenEditing!: boolean; |
|
|
|
|
|
|
|
mode!: NumberInputMode; |
|
|
|
incrementBehavior!: NumberInputIncrementBehavior; |
|
|
|
step!: number; |
|
|
|
rangeMin!: number | undefined; |
|
|
|
rangeMax!: number | undefined; |
|
|
|
|
|
|
|
minWidth!: number; |
|
|
|
maxWidth!: number; |
|
} |
|
|
|
export class NodeCatalog extends WidgetProps { |
|
disabled!: boolean; |
|
} |
|
|
|
export class PopoverButton extends WidgetProps { |
|
style!: PopoverButtonStyle | undefined; |
|
|
|
menuDirection!: MenuDirection | undefined; |
|
|
|
icon!: IconName | undefined; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
|
|
popoverLayout!: LayoutGroup[]; |
|
|
|
popoverMinWidth: number | undefined; |
|
} |
|
|
|
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center"; |
|
|
|
export type RadioEntryData = { |
|
value?: string; |
|
label?: string; |
|
icon?: IconName; |
|
tooltip?: string; |
|
|
|
|
|
action?: () => void; |
|
}; |
|
export type RadioEntries = RadioEntryData[]; |
|
|
|
export class RadioInput extends WidgetProps { |
|
entries!: RadioEntries; |
|
|
|
disabled!: boolean; |
|
|
|
selectedIndex!: number | undefined; |
|
|
|
minWidth!: number; |
|
} |
|
|
|
export type SeparatorDirection = "Horizontal" | "Vertical"; |
|
export type SeparatorType = "Related" | "Unrelated" | "Section"; |
|
|
|
export class Separator extends WidgetProps { |
|
direction!: SeparatorDirection; |
|
|
|
type!: SeparatorType; |
|
} |
|
|
|
export class WorkingColorsInput extends WidgetProps { |
|
@Type(() => Color) |
|
primary!: Color; |
|
|
|
@Type(() => Color) |
|
secondary!: Color; |
|
} |
|
|
|
export class TextAreaInput extends WidgetProps { |
|
value!: string; |
|
|
|
label!: string | undefined; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class ParameterExposeButton extends WidgetProps { |
|
exposed!: boolean; |
|
|
|
dataType!: FrontendGraphDataType; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class TextButton extends WidgetProps { |
|
label!: string; |
|
|
|
icon!: IconName | undefined; |
|
|
|
hoverIcon!: IconName | undefined; |
|
|
|
emphasized!: boolean; |
|
|
|
flush!: boolean; |
|
|
|
minWidth!: number; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
menuListChildren!: MenuListEntry[][]; |
|
} |
|
|
|
export type TextButtonWidget = { |
|
tooltip?: string; |
|
message?: string | object; |
|
callback?: () => void; |
|
props: { |
|
kind: "TextButton"; |
|
label: string; |
|
icon?: IconName; |
|
emphasized?: boolean; |
|
flush?: boolean; |
|
minWidth?: number; |
|
disabled?: boolean; |
|
tooltip?: string; |
|
|
|
|
|
|
|
}; |
|
}; |
|
|
|
export class BreadcrumbTrailButtons extends WidgetProps { |
|
labels!: string[]; |
|
|
|
disabled!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class TextInput extends WidgetProps { |
|
value!: string; |
|
|
|
label!: string | undefined; |
|
|
|
disabled!: boolean; |
|
|
|
minWidth!: number; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
} |
|
|
|
export class TextLabel extends WidgetProps { |
|
|
|
value!: string; |
|
|
|
|
|
disabled!: boolean; |
|
|
|
bold!: boolean; |
|
|
|
italic!: boolean; |
|
|
|
centerAlign!: boolean; |
|
|
|
tableAlign!: boolean; |
|
|
|
minWidth!: number; |
|
|
|
multiline!: boolean; |
|
|
|
@Transform(({ value }: { value: string }) => value || undefined) |
|
tooltip!: string | undefined; |
|
|
|
checkboxId!: bigint | undefined; |
|
} |
|
|
|
export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"; |
|
|
|
export class ReferencePointInput extends WidgetProps { |
|
value!: ReferencePoint; |
|
|
|
disabled!: boolean; |
|
} |
|
|
|
|
|
|
|
const widgetSubTypes = [ |
|
{ value: BreadcrumbTrailButtons, name: "BreadcrumbTrailButtons" }, |
|
{ value: CheckboxInput, name: "CheckboxInput" }, |
|
{ value: ColorInput, name: "ColorInput" }, |
|
{ value: CurveInput, name: "CurveInput" }, |
|
{ value: DropdownInput, name: "DropdownInput" }, |
|
{ value: FontInput, name: "FontInput" }, |
|
{ value: IconButton, name: "IconButton" }, |
|
{ value: ImageButton, name: "ImageButton" }, |
|
{ value: IconLabel, name: "IconLabel" }, |
|
{ value: NodeCatalog, name: "NodeCatalog" }, |
|
{ value: NumberInput, name: "NumberInput" }, |
|
{ value: ParameterExposeButton, name: "ParameterExposeButton" }, |
|
{ value: ReferencePointInput, name: "ReferencePointInput" }, |
|
{ value: PopoverButton, name: "PopoverButton" }, |
|
{ value: RadioInput, name: "RadioInput" }, |
|
{ value: Separator, name: "Separator" }, |
|
{ value: WorkingColorsInput, name: "WorkingColorsInput" }, |
|
{ value: TextAreaInput, name: "TextAreaInput" }, |
|
{ value: TextButton, name: "TextButton" }, |
|
{ value: TextInput, name: "TextInput" }, |
|
{ value: TextLabel, name: "TextLabel" }, |
|
] as const; |
|
|
|
type WidgetSubTypes = (typeof widgetSubTypes)[number]; |
|
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> }; |
|
export type WidgetPropsNames = keyof WidgetKindMap; |
|
export type WidgetPropsSet = WidgetKindMap[WidgetPropsNames]; |
|
|
|
export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) { |
|
if (props.kind === kind) return props as WidgetKindMap[K]; |
|
else return undefined; |
|
} |
|
|
|
export class Widget { |
|
constructor(props: WidgetPropsSet, widgetId: bigint) { |
|
this.props = props; |
|
this.widgetId = widgetId; |
|
} |
|
|
|
@Type(() => WidgetProps, { discriminator: { property: "kind", subTypes: [...widgetSubTypes] }, keepDiscriminatorProperty: true }) |
|
props!: WidgetPropsSet; |
|
|
|
widgetId!: bigint; |
|
} |
|
|
|
|
|
function hoistWidgetHolder(widgetHolder: any): Widget { |
|
const kind = Object.keys(widgetHolder.widget)[0]; |
|
const props = widgetHolder.widget[kind]; |
|
props.kind = kind; |
|
|
|
if (kind === "PopoverButton") { |
|
props.popoverLayout = props.popoverLayout.map(createLayoutGroup); |
|
} |
|
|
|
const { widgetId } = widgetHolder; |
|
|
|
return plainToClass(Widget, { props, widgetId }); |
|
} |
|
|
|
|
|
function hoistWidgetHolders(widgetHolders: any[]): Widget[] { |
|
return widgetHolders.map(hoistWidgetHolder); |
|
} |
|
|
|
|
|
|
|
export type WidgetLayout = { |
|
layoutTarget: unknown; |
|
layout: LayoutGroup[]; |
|
}; |
|
|
|
export class WidgetDiffUpdate extends JsMessage { |
|
layoutTarget!: unknown; |
|
|
|
|
|
|
|
@Transform(({ value }: { value: any }) => createWidgetDiff(value)) |
|
diff!: WidgetDiff[]; |
|
} |
|
|
|
type UIItem = LayoutGroup[] | LayoutGroup | Widget | Widget[] | MenuBarEntry[] | MenuBarEntry; |
|
type WidgetDiff = { widgetPath: number[]; newValue: UIItem }; |
|
|
|
export function defaultWidgetLayout(): WidgetLayout { |
|
return { |
|
layoutTarget: undefined, |
|
layout: [], |
|
}; |
|
} |
|
|
|
|
|
export function patchWidgetLayout(layout: WidgetLayout, updates: WidgetDiffUpdate) { |
|
layout.layoutTarget = updates.layoutTarget; |
|
|
|
updates.diff.forEach((update) => { |
|
|
|
const diffObject = update.widgetPath.reduce((targetLayout, index) => { |
|
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; |
|
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; |
|
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; |
|
if ("layout" in targetLayout) return targetLayout.layout[index]; |
|
if (targetLayout instanceof Widget) { |
|
if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) { |
|
return targetLayout.props.popoverLayout[index]; |
|
} |
|
|
|
console.error("Tried to index widget"); |
|
return targetLayout; |
|
} |
|
|
|
|
|
if ("action" in targetLayout) return targetLayout.children![index]; |
|
return targetLayout[index]; |
|
}, layout.layout as UIItem); |
|
|
|
|
|
if ("length" in diffObject) { |
|
diffObject.length = 0; |
|
} |
|
|
|
|
|
Object.keys(diffObject).forEach((key) => delete (diffObject as any)[key]); |
|
|
|
|
|
|
|
|
|
Object.assign(diffObject, update.newValue); |
|
}); |
|
} |
|
|
|
export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection; |
|
|
|
export type WidgetSpanColumn = { columnWidgets: Widget[] }; |
|
export function isWidgetSpanColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetSpanColumn { |
|
return Boolean((layoutColumn as WidgetSpanColumn)?.columnWidgets); |
|
} |
|
|
|
export type WidgetSpanRow = { rowWidgets: Widget[] }; |
|
export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpanRow { |
|
return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets); |
|
} |
|
|
|
export type WidgetTable = { tableWidgets: Widget[][] }; |
|
export function isWidgetTable(layoutTable: LayoutGroup): layoutTable is WidgetTable { |
|
return Boolean((layoutTable as WidgetTable)?.tableWidgets); |
|
} |
|
|
|
export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: LayoutGroup[] }; |
|
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection { |
|
return Boolean((layoutRow as WidgetSection)?.layout); |
|
} |
|
|
|
|
|
|
|
function createWidgetDiff(diffs: any[]): WidgetDiff[] { |
|
return diffs.map((diff) => { |
|
const { widgetPath, newValue } = diff; |
|
if (newValue.subLayout) { |
|
return { widgetPath, newValue: newValue.subLayout.map(createLayoutGroup) }; |
|
} |
|
if (newValue.layoutGroup) { |
|
return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) }; |
|
} |
|
if (newValue.widget) { |
|
return { widgetPath, newValue: hoistWidgetHolder(newValue.widget) }; |
|
} |
|
|
|
throw new Error("DiffUpdate invalid"); |
|
}); |
|
} |
|
|
|
|
|
|
|
function createLayoutGroup(layoutGroup: any): LayoutGroup { |
|
if (layoutGroup.column) { |
|
const columnWidgets = hoistWidgetHolders(layoutGroup.column.columnWidgets); |
|
|
|
const result: WidgetSpanColumn = { columnWidgets }; |
|
return result; |
|
} |
|
|
|
if (layoutGroup.row) { |
|
const result: WidgetSpanRow = { rowWidgets: hoistWidgetHolders(layoutGroup.row.rowWidgets) }; |
|
return result; |
|
} |
|
|
|
if (layoutGroup.section) { |
|
const result: WidgetSection = { |
|
name: layoutGroup.section.name, |
|
description: layoutGroup.section.description, |
|
visible: layoutGroup.section.visible, |
|
pinned: layoutGroup.section.pinned, |
|
id: layoutGroup.section.id, |
|
layout: layoutGroup.section.layout.map(createLayoutGroup), |
|
}; |
|
return result; |
|
} |
|
|
|
if (layoutGroup.table) { |
|
const result: WidgetTable = { |
|
tableWidgets: layoutGroup.table.tableWidgets.map(hoistWidgetHolders), |
|
}; |
|
return result; |
|
} |
|
|
|
throw new Error("Layout row type does not exist"); |
|
} |
|
|
|
|
|
export class UpdateDialogButtons extends WidgetDiffUpdate {} |
|
|
|
export class UpdateDialogColumn1 extends WidgetDiffUpdate {} |
|
|
|
export class UpdateDialogColumn2 extends WidgetDiffUpdate {} |
|
|
|
export class UpdateDocumentBarLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateDocumentModeLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate {} |
|
|
|
|
|
export class UpdateMenuBarLayout extends JsMessage { |
|
layoutTarget!: unknown; |
|
|
|
|
|
|
|
@Transform(({ value }: { value: any }) => createMenuLayout(value)) |
|
layout!: MenuBarEntry[]; |
|
} |
|
|
|
export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateToolShelfLayout extends WidgetDiffUpdate {} |
|
|
|
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {} |
|
|
|
|
|
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] { |
|
return menuBarEntry.map((entry) => ({ |
|
...entry, |
|
children: createMenuLayoutRecursive(entry.children), |
|
})); |
|
} |
|
|
|
function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] { |
|
return children.map((groups) => |
|
groups.map((entry) => ({ |
|
...entry, |
|
action: hoistWidgetHolders([entry.action])[0], |
|
children: entry.children ? createMenuLayoutRecursive(entry.children) : undefined, |
|
disabled: entry.disabled ?? false, |
|
})), |
|
); |
|
} |
|
|
|
|
|
|
|
type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage; |
|
type MessageMaker = typeof JsMessage | JSMessageFactory; |
|
|
|
export const messageMakers: Record<string, MessageMaker> = { |
|
ClearAllNodeGraphWires, |
|
DisplayDialog, |
|
DisplayDialogDismiss, |
|
DisplayDialogPanic, |
|
DisplayEditableTextbox, |
|
DisplayEditableTextboxTransform, |
|
DisplayRemoveEditableTextbox, |
|
SendUIMetadata, |
|
TriggerAboutGraphiteLocalizedCommitDate, |
|
TriggerDelayedZoomCanvasToFitAll, |
|
TriggerDownloadImage, |
|
TriggerDownloadTextFile, |
|
TriggerFetchAndOpenDocument, |
|
TriggerFontLoad, |
|
TriggerImport, |
|
TriggerIndexedDbRemoveDocument, |
|
TriggerIndexedDbWriteDocument, |
|
TriggerLoadFirstAutoSaveDocument, |
|
TriggerLoadPreferences, |
|
TriggerLoadRestAutoSaveDocuments, |
|
TriggerOpenDocument, |
|
TriggerPaste, |
|
TriggerSaveActiveDocument, |
|
TriggerSavePreferences, |
|
TriggerTextCommit, |
|
TriggerTextCopy, |
|
TriggerVisitLink, |
|
UpdateActiveDocument, |
|
UpdateBox, |
|
UpdateClickTargets, |
|
UpdateContextMenuInformation, |
|
UpdateDialogButtons, |
|
UpdateDialogColumn1, |
|
UpdateDialogColumn2, |
|
UpdateDocumentArtwork, |
|
UpdateDocumentBarLayout, |
|
UpdateDocumentLayerDetails, |
|
UpdateDocumentLayerStructureJs, |
|
UpdateDocumentModeLayout, |
|
UpdateDocumentRulers, |
|
UpdateDocumentScrollbars, |
|
UpdateExportReorderIndex, |
|
UpdateEyedropperSamplingState, |
|
UpdateGraphFadeArtwork, |
|
UpdateGraphViewOverlay, |
|
UpdateSpreadsheetState, |
|
UpdateImportReorderIndex, |
|
UpdateImportsExports, |
|
UpdateInputHints, |
|
UpdateInSelectedNetwork, |
|
UpdateLayersPanelControlBarLeftLayout, |
|
UpdateLayersPanelControlBarRightLayout, |
|
UpdateLayersPanelBottomBarLayout, |
|
UpdateLayerWidths, |
|
UpdateMenuBarLayout, |
|
UpdateMouseCursor, |
|
UpdateNodeGraphNodes, |
|
UpdateVisibleNodes, |
|
UpdateNodeGraphWires, |
|
UpdateNodeGraphTransform, |
|
UpdateNodeGraphControlBarLayout, |
|
UpdateNodeGraphSelection, |
|
UpdateNodeThumbnail, |
|
UpdateOpenDocumentsList, |
|
UpdatePropertyPanelSectionsLayout, |
|
UpdateSpreadsheetLayout, |
|
UpdateToolOptionsLayout, |
|
UpdateToolShelfLayout, |
|
UpdateWirePathInProgress, |
|
UpdateWorkingColorsLayout, |
|
} as const; |
|
export type JsMessageType = keyof typeof messageMakers; |
|
|