mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 03:38:11 +08:00
Refactor messages.ts by removing class-transformer and JS classes (#3858)
* Fix gamma correction with HTML-based editable Text tool text * Migrate simple, undecorated classes to types * Remove TupleToVec2 transformation * Remove @Transform from tooltips * Cleanup: replace value.toString() with String(value) everywhere * Convert documentId from string to bigint * Migrate the rest of the easy @Transform/@Type decorations * Migrate FillChoice * Migrate WidgetDiffUpdate * Migrate WidgetInstance * Migrate away from classes that extend WidgetProps * Remove class-transformer and all classes in messages.ts * Migrate UI layout passing * Remove dead code * Remove unnecessary export and readonly prefixes * Remove HSVA type * Break out Color, Gradient, and FillChoice functions into a utility-functions file * Move widget helper functions from messages.ts into a new utility-functions file; restructure type imports * Reduce internal type defs * Rename JsMessage to FrontendMessage * Code review fixes * Fix other usages * Tidying up
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
import { type Editor as GraphiteEditor, initWasm, createEditor } from "@graphite/editor";
|
||||
import { initWasm, createEditor } from "@graphite/editor";
|
||||
import type { Editor as GraphiteEditor } from "@graphite/editor";
|
||||
|
||||
import Editor from "@graphite/components/Editor.svelte";
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Defines the message formats and data types received from the backend. Since Rust
|
||||
|
||||
## Subscription router: `subscription-router.ts`
|
||||
|
||||
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeJsMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. This file's other exported function, `handleJsMessage(messageType, messageData, wasm, instance)`, is called in `editor.ts` by the associated editor instance when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber for given `messageType` by executing its registered `callback` function. As an argument to the function, it provides the `messageData` payload transformed into its TypeScript-friendly format defined in `messages.ts`.
|
||||
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. This file's other exported function, `handleFrontendMessage(messageType, messageData, wasm, instance)`, is called in `editor.ts` by the associated editor instance when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber for given `messageType` by executing its registered `callback` function. As an argument to the function, it provides the `messageData` payload transformed into its TypeScript-friendly format defined in `messages.ts`.
|
||||
|
||||
## Svelte app entry point: `App.svelte`
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, setContext } from "svelte";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { createClipboardManager } from "@graphite/io-managers/clipboard";
|
||||
import { createHyperlinkManager } from "@graphite/io-managers/hyperlink";
|
||||
import { createInputManager } from "@graphite/io-managers/input";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
|
||||
import type { FillChoice, MenuDirection } from "@graphite/messages";
|
||||
import type { Color } from "@graphite/messages";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import {
|
||||
type Color,
|
||||
contrastingOutlineFactor,
|
||||
isColor,
|
||||
isGradient,
|
||||
@@ -12,7 +13,7 @@
|
||||
createColorFromHSVA,
|
||||
colorFromCSS,
|
||||
colorToRgb255,
|
||||
colorToHSVA,
|
||||
colorToHSV,
|
||||
colorToHexOptionalAlpha,
|
||||
colorToHexNoAlpha,
|
||||
colorToRgbCSS,
|
||||
@@ -20,8 +21,8 @@
|
||||
colorOpaque,
|
||||
colorEquals,
|
||||
gradientFirstColor,
|
||||
} from "@graphite/messages";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
} from "@graphite/utility-functions/colors";
|
||||
import type { HSV, RGB } from "@graphite/utility-functions/colors";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
import { isDesktop } from "@graphite/utility-functions/platform";
|
||||
|
||||
@@ -70,8 +71,8 @@
|
||||
export let open: boolean;
|
||||
|
||||
const colorForHSVA = isColor(colorOrGradient) ? colorOrGradient : gradientFirstColor(colorOrGradient);
|
||||
const hsvaOrNone = colorForHSVA ? colorToHSVA(colorForHSVA) : undefined;
|
||||
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
||||
const hsvOrNone = colorForHSVA ? colorToHSV(colorForHSVA) : undefined;
|
||||
const hsv = hsvOrNone || { h: 0, s: 0, v: 0 };
|
||||
|
||||
// Gradient color stops
|
||||
$: gradient = isGradient(colorOrGradient) ? colorOrGradient : undefined;
|
||||
@@ -81,17 +82,17 @@
|
||||
// Currently viewed color
|
||||
$: color = isColor(colorOrGradient) ? colorOrGradient : selectedGradientColor;
|
||||
// New color components
|
||||
let hue = hsva.h;
|
||||
let saturation = hsva.s;
|
||||
let value = hsva.v;
|
||||
let alpha = hsva.a;
|
||||
let isNone = hsvaOrNone === undefined;
|
||||
let hue = hsv.h;
|
||||
let saturation = hsv.s;
|
||||
let value = hsv.v;
|
||||
let alpha = colorForHSVA ? colorForHSVA.alpha : 1;
|
||||
let isNone = hsvOrNone === undefined;
|
||||
// Old color components
|
||||
let oldHue = hsva.h;
|
||||
let oldSaturation = hsva.s;
|
||||
let oldValue = hsva.v;
|
||||
let oldAlpha = hsva.a;
|
||||
let oldIsNone = hsvaOrNone === undefined;
|
||||
let oldHue = hsv.h;
|
||||
let oldSaturation = hsv.s;
|
||||
let oldValue = hsv.v;
|
||||
let oldAlpha = colorForHSVA ? colorForHSVA.alpha : 1;
|
||||
let oldIsNone = hsvOrNone === undefined;
|
||||
// Transient state
|
||||
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
||||
let strayCloses = true;
|
||||
@@ -114,8 +115,8 @@
|
||||
$: watchOpen(open);
|
||||
$: watchColor(color);
|
||||
|
||||
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
||||
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
||||
$: oldColor = oldIsNone ? createNoneColor() : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha);
|
||||
$: newColor = isNone ? createNoneColor() : createColorFromHSVA(hue, saturation, value, alpha);
|
||||
$: rgbChannels = Object.entries(colorToRgb255(newColor) || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
||||
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
||||
$: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1);
|
||||
@@ -123,11 +124,6 @@
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
||||
|
||||
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
||||
if (none) return createNoneColor();
|
||||
return createColorFromHSVA(h, s, v, a);
|
||||
}
|
||||
|
||||
async function watchOpen(open: boolean) {
|
||||
if (open) {
|
||||
setTimeout(() => hexCodeInputWidget?.focus(), 0);
|
||||
@@ -138,9 +134,9 @@
|
||||
}
|
||||
|
||||
function watchColor(color: Color) {
|
||||
const hsva = colorToHSVA(color);
|
||||
const hsv = colorToHSV(color);
|
||||
|
||||
if (hsva === undefined) {
|
||||
if (hsv === undefined) {
|
||||
setNewHSVA(0, 0, 0, 1, true);
|
||||
return;
|
||||
}
|
||||
@@ -149,14 +145,14 @@
|
||||
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom)
|
||||
// - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent
|
||||
// - ...reset the hue to 0° if the color's value is black, where all hues are equivalent
|
||||
if (!(hsva.h === 0 && hue === 1) && hsva.s > 0 && hsva.v > 0) hue = hsva.h;
|
||||
if (!(hsv.h === 0 && hue === 1) && hsv.s > 0 && hsv.v > 0) hue = hsv.h;
|
||||
// Update the saturation, but only if it is necessary so we don't:
|
||||
// - ...reset the saturation to the left if the color's value is black along the bottom edge, where all saturations are equivalent
|
||||
if (hsva.v !== 0) saturation = hsva.s;
|
||||
if (hsv.v !== 0) saturation = hsv.s;
|
||||
// Update the value
|
||||
value = hsva.v;
|
||||
value = hsv.v;
|
||||
// Update the alpha
|
||||
alpha = hsva.a;
|
||||
alpha = color.alpha;
|
||||
// Update the status of this not being a color
|
||||
isNone = false;
|
||||
}
|
||||
@@ -375,9 +371,10 @@
|
||||
setColor(createNoneColor());
|
||||
} else {
|
||||
const presetColor = createColor(...PURE_COLORS[preset], 1);
|
||||
const hsva = colorToHSVA(presetColor) || { h: 0, s: 0, v: 0, a: 0 };
|
||||
const hsv = colorToHSV(presetColor);
|
||||
if (!hsv) return;
|
||||
|
||||
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
|
||||
setNewHSVA(hsv.h, hsv.s, hsv.v, presetColor.alpha, false);
|
||||
setColor(presetColor);
|
||||
}
|
||||
}
|
||||
@@ -425,13 +422,13 @@
|
||||
activeIndexIsMidpoint = activeMarkerIsMidpoint;
|
||||
|
||||
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
|
||||
const hsva = color ? colorToHSVA(color) : undefined;
|
||||
if (!color || !hsva) return;
|
||||
const hsv = color ? colorToHSV(color) : undefined;
|
||||
if (!color || !hsv) return;
|
||||
|
||||
setColor(color);
|
||||
|
||||
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
|
||||
setOldHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none);
|
||||
setNewHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none);
|
||||
setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none);
|
||||
}
|
||||
|
||||
export function div(): HTMLDivElement | undefined {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { patchLayout, UpdateDataPanelLayout, type Layout } from "@graphite/messages";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
@@ -12,14 +13,14 @@
|
||||
let dataPanelLayout: Layout = [];
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDataPanelLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
|
||||
patchLayout(dataPanelLayout, data);
|
||||
dataPanelLayout = dataPanelLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDataPanelLayout);
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("DataPanel");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,33 +2,15 @@
|
||||
import { getContext, onMount, onDestroy, tick } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import {
|
||||
type MenuDirection,
|
||||
type MouseCursorIcon,
|
||||
type XY,
|
||||
type Color,
|
||||
isColor,
|
||||
createColor,
|
||||
colorToHexOptionalAlpha,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerTextCommit,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateGradientStopColorPickerPosition,
|
||||
UpdateMouseCursor,
|
||||
isWidgetSpanRow,
|
||||
} from "@graphite/messages";
|
||||
import type { Color, FrontendMessages, MenuDirection } from "@graphite/messages";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import { isColor, createColor } from "@graphite/utility-functions/colors";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||
import { isWidgetSpanRow } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
||||
@@ -39,6 +21,8 @@
|
||||
import ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
type DisplayEditableTextbox = FrontendMessages["DisplayEditableTextbox"];
|
||||
|
||||
let rulerHorizontal: RulerInput | undefined;
|
||||
let rulerVertical: RulerInput | undefined;
|
||||
let viewport: HTMLDivElement | undefined;
|
||||
@@ -54,12 +38,12 @@
|
||||
let textInputMatrix: number[];
|
||||
|
||||
// Scrollbars
|
||||
let scrollbarPos: XY = { x: 0.5, y: 0.5 };
|
||||
let scrollbarSize: XY = { x: 0.5, y: 0.5 };
|
||||
let scrollbarMultiplier: XY = { x: 0, y: 0 };
|
||||
let scrollbarPos = { x: 0.5, y: 0.5 };
|
||||
let scrollbarSize = { x: 0.5, y: 0.5 };
|
||||
let scrollbarMultiplier = { x: 0, y: 0 };
|
||||
|
||||
// Rulers
|
||||
let rulerOrigin: XY = { x: 0, y: 0 };
|
||||
let rulerOrigin = { x: 0, y: 0 };
|
||||
let rulerSpacing = 100;
|
||||
let rulerInterval = 100;
|
||||
let rulersVisible = true;
|
||||
@@ -227,7 +211,7 @@
|
||||
export async function updateEyedropperSamplingState(
|
||||
// `image` is currently only used for Vello renders
|
||||
image: ImageData | undefined,
|
||||
mousePosition: XY | undefined,
|
||||
mousePosition: [number, number] | undefined,
|
||||
colorPrimary: string,
|
||||
colorSecondary: string,
|
||||
): Promise<[number, number, number] | undefined> {
|
||||
@@ -239,8 +223,8 @@
|
||||
|
||||
if (canvasWidth === undefined || canvasHeight === undefined) return undefined;
|
||||
|
||||
cursorLeft = mousePosition.x;
|
||||
cursorTop = mousePosition.y;
|
||||
cursorLeft = mousePosition[0];
|
||||
cursorTop = mousePosition[1];
|
||||
|
||||
let preview = image;
|
||||
if (!preview) {
|
||||
@@ -262,8 +246,8 @@
|
||||
if (!rasterizedContext) return undefined;
|
||||
|
||||
preview = rasterizedContext.getImageData(
|
||||
mousePosition.x * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
mousePosition.y * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
mousePosition[0] * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
mousePosition[1] * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
|
||||
ZOOM_WINDOW_DIMENSIONS,
|
||||
ZOOM_WINDOW_DIMENSIONS,
|
||||
);
|
||||
@@ -293,25 +277,41 @@
|
||||
}
|
||||
|
||||
// Update scrollbars and rulers
|
||||
export function updateDocumentScrollbars(position: XY, size: XY, multiplier: XY) {
|
||||
scrollbarPos = position;
|
||||
scrollbarSize = size;
|
||||
scrollbarMultiplier = multiplier;
|
||||
export function updateDocumentScrollbars(position: [number, number], size: [number, number], multiplier: [number, number]) {
|
||||
scrollbarPos = { x: position[0], y: position[1] };
|
||||
scrollbarSize = { x: size[0], y: size[1] };
|
||||
scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
|
||||
}
|
||||
|
||||
export function updateDocumentRulers(origin: XY, spacing: number, interval: number, visible: boolean) {
|
||||
rulerOrigin = origin;
|
||||
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean) {
|
||||
rulerOrigin = { x: origin[0], y: origin[1] };
|
||||
rulerSpacing = spacing;
|
||||
rulerInterval = interval;
|
||||
rulersVisible = visible;
|
||||
}
|
||||
|
||||
// Update mouse cursor icon
|
||||
export function updateMouseCursor(cursor: MouseCursorIcon) {
|
||||
let cursorString: string = cursor;
|
||||
export function updateMouseCursor(cursor: string) {
|
||||
const mouseCursorIconCSSNames: Record<string, string> = {
|
||||
Default: "default",
|
||||
Alias: "alias",
|
||||
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",
|
||||
};
|
||||
let cursorString = mouseCursorIconCSSNames[cursor] || mouseCursorIconCSSNames["Alias"];
|
||||
|
||||
// This isn't very clean but it's good enough for now until we need more icons, then we can build something more robust (consider blob URLs)
|
||||
if (cursor === "custom-rotate") {
|
||||
if (cursor === "Rotate") {
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20">
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M10,15.8c-3.2,0-5.8-2.6-5.8-5.8S6.8,4.2,10,4.2c0.999,0,1.999,0.273,2.877,0.771L11.7,7h5.8l-2.9-5l-1.013,1.746C12.5,3.125,11.271,2.8,10,2.8C6,2.8,2.8,6,2.8,10S6,17.2,10,17.2s7.2-3.2,7.2-7.2h-1.4C15.8,13.2,13.2,15.8,10,15.8z" />
|
||||
@@ -363,7 +363,7 @@
|
||||
textInput.style.height = height;
|
||||
textInput.style.lineHeight = `${data.lineHeightRatio}`;
|
||||
textInput.style.fontSize = `${data.fontSize}px`;
|
||||
textInput.style.color = colorToHexOptionalAlpha(data.color) || "transparent";
|
||||
textInput.style.color = data.color;
|
||||
textInput.style.textAlign = data.align;
|
||||
|
||||
textInput.oninput = () => {
|
||||
@@ -418,7 +418,7 @@
|
||||
// which provides pixel-perfect physical dimensions via devicePixelContentBoxSize
|
||||
}
|
||||
|
||||
function gradientStopPickerDirection(position: XY | undefined, viewport: HTMLDivElement | undefined): MenuDirection {
|
||||
function gradientStopPickerDirection(position: { x: number; y: number } | undefined, viewport: HTMLDivElement | undefined): MenuDirection {
|
||||
const picker = (gradientStopPicker?.div()?.querySelector("[data-floating-menu-content]") || undefined) as HTMLElement | undefined;
|
||||
if (!picker || !position || !viewport) return "Bottom";
|
||||
|
||||
@@ -448,12 +448,12 @@
|
||||
updatePixelRatio();
|
||||
|
||||
// Update rendered SVGs
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentArtwork", async (data) => {
|
||||
await tick();
|
||||
|
||||
updateDocumentArtwork(data.svg);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateEyedropperSamplingState", async (data) => {
|
||||
await tick();
|
||||
|
||||
const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data;
|
||||
@@ -467,19 +467,19 @@
|
||||
});
|
||||
|
||||
// Gradient stop color picker
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGradientStopColorPickerPosition, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateGradientStopColorPickerPosition", (data) => {
|
||||
gradientStopPickerColor = data.color;
|
||||
gradientStopPickerPosition = { x: data.x, y: data.y };
|
||||
});
|
||||
|
||||
// Update scrollbars and rulers
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentScrollbars", async (data) => {
|
||||
await tick();
|
||||
|
||||
const { position, size, multiplier } = data;
|
||||
updateDocumentScrollbars(position, size, multiplier);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
|
||||
await tick();
|
||||
|
||||
const { origin, spacing, interval, visible } = data;
|
||||
@@ -487,25 +487,24 @@
|
||||
});
|
||||
|
||||
// Update mouse cursor icon
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMouseCursor, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateMouseCursor", async (data) => {
|
||||
await tick();
|
||||
|
||||
const { cursor } = data;
|
||||
updateMouseCursor(cursor);
|
||||
updateMouseCursor(data.cursor);
|
||||
});
|
||||
|
||||
// Text entry
|
||||
editor.subscriptions.subscribeJsMessage(TriggerTextCommit, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerTextCommit", async () => {
|
||||
await tick();
|
||||
|
||||
triggerTextCommit();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextbox", async (data) => {
|
||||
await tick();
|
||||
|
||||
displayEditableTextbox(data);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxUpdateFontData, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxUpdateFontData", async (data) => {
|
||||
await tick();
|
||||
|
||||
const fontData = new Uint8Array(data.fontData);
|
||||
@@ -514,10 +513,10 @@
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxTransform", async (data) => {
|
||||
textInputMatrix = data.transform;
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayRemoveEditableTextbox", async () => {
|
||||
await tick();
|
||||
|
||||
displayRemoveEditableTextbox();
|
||||
|
||||
@@ -3,19 +3,12 @@
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import {
|
||||
patchLayout,
|
||||
UpdateDocumentLayerDetails,
|
||||
UpdateDocumentLayerStructure,
|
||||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
} from "@graphite/messages";
|
||||
import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/messages";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
@@ -76,26 +69,26 @@
|
||||
let layersPanelBottomBarLayout: Layout = [];
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlLeftBar", (data) => {
|
||||
patchLayout(layersPanelControlBarLeftLayout, data);
|
||||
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarRightLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlRightBar", (data) => {
|
||||
patchLayout(layersPanelControlBarRightLayout, data);
|
||||
layersPanelControlBarRightLayout = layersPanelControlBarRightLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelBottomBarLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("LayersPanelBottomBar", (data) => {
|
||||
patchLayout(layersPanelBottomBarLayout, data);
|
||||
layersPanelBottomBarLayout = layersPanelBottomBarLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructure, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerStructure", (data) => {
|
||||
rebuildLayerHierarchy(data.layerStructure);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerDetails", (data) => {
|
||||
const targetLayer = data.data;
|
||||
const targetId = targetLayer.id;
|
||||
|
||||
@@ -114,11 +107,11 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructure);
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails);
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlLeftBar");
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlRightBar");
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelBottomBar");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerStructure");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerDetails");
|
||||
|
||||
removeEventListener("pointerup", draggingPointerUp);
|
||||
removeEventListener("pointermove", draggingPointerMove);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { patchLayout, UpdatePropertiesPanelLayout, type Layout } from "@graphite/messages";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
@@ -12,14 +13,14 @@
|
||||
let propertiesPanelLayout: Layout = [];
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("PropertiesPanel", (data) => {
|
||||
patchLayout(propertiesPanelLayout, data);
|
||||
propertiesPanelLayout = propertiesPanelLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout);
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("PropertiesPanel");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { isDesktop } from "@graphite/utility-functions/platform";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
@@ -18,14 +18,14 @@
|
||||
let welcomePanelButtonsLayout: Layout = [];
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWelcomeScreenButtonsLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("WelcomeScreenButtons", (data) => {
|
||||
patchLayout(welcomePanelButtonsLayout, data);
|
||||
welcomePanelButtonsLayout = welcomePanelButtonsLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeJsMessage(UpdateWelcomeScreenButtonsLayout);
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("WelcomeScreenButtons");
|
||||
});
|
||||
|
||||
function dropFile(e: DragEvent) {
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
function createNode(identifier: string) {
|
||||
if ($nodeGraph.contextMenuInformation === undefined) return;
|
||||
|
||||
editor.handle.createNode(identifier, $nodeGraph.contextMenuInformation.contextMenuCoordinates.x, $nodeGraph.contextMenuInformation.contextMenuCoordinates.y);
|
||||
editor.handle.createNode(identifier, $nodeGraph.contextMenuInformation.contextMenuCoordinates[0], $nodeGraph.contextMenuInformation.contextMenuCoordinates[1]);
|
||||
}
|
||||
|
||||
function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string {
|
||||
@@ -203,8 +203,8 @@
|
||||
class="context-menu"
|
||||
data-context-menu
|
||||
styles={{
|
||||
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
|
||||
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
|
||||
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[0] * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
|
||||
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[1] * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
|
||||
}}
|
||||
open={true}
|
||||
type="Popover"
|
||||
@@ -257,10 +257,10 @@
|
||||
|
||||
{#if $nodeGraph.error}
|
||||
<div class="node-error-container" style:transform-origin="0 0" style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
|
||||
<span class="node-error faded" style:left={`${$nodeGraph.error.position.x}px`} style:top={`${$nodeGraph.error.position.y}px`} transition:fade={FADE_TRANSITION}>
|
||||
<span class="node-error faded" style:left={`${$nodeGraph.error.position[0]}px`} style:top={`${$nodeGraph.error.position[1]}px`} transition:fade={FADE_TRANSITION}>
|
||||
{$nodeGraph.error.error}
|
||||
</span>
|
||||
<span class="node-error hover" style:left={`${$nodeGraph.error.position.x}px`} style:top={`${$nodeGraph.error.position.y}px`} transition:fade={FADE_TRANSITION}>
|
||||
<span class="node-error hover" style:left={`${$nodeGraph.error.position[0]}px`} style:top={`${$nodeGraph.error.position[1]}px`} transition:fade={FADE_TRANSITION}>
|
||||
{$nodeGraph.error.error}
|
||||
</span>
|
||||
</div>
|
||||
@@ -324,8 +324,8 @@
|
||||
data-datatype={frontendOutput.dataType}
|
||||
style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 8) / 24 + index}
|
||||
>
|
||||
{#if frontendOutput.connectedTo.length > 0}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
@@ -340,8 +340,8 @@
|
||||
class="edit-import-export import"
|
||||
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
|
||||
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 8) / 24 + index}
|
||||
>
|
||||
{#if editingNameImportIndex === index}
|
||||
<input
|
||||
@@ -377,8 +377,8 @@
|
||||
{:else}
|
||||
<div
|
||||
class="plus"
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
|
||||
>
|
||||
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} />
|
||||
</div>
|
||||
@@ -397,8 +397,8 @@
|
||||
data-datatype={frontendInput.dataType}
|
||||
style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 8) / 24 + index}
|
||||
>
|
||||
{#if frontendInput.connectedTo !== "Connected to nothing."}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
@@ -412,8 +412,8 @@
|
||||
class="edit-import-export export"
|
||||
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
|
||||
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 8) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 8) / 24 + index}
|
||||
>
|
||||
{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
|
||||
{#if index > 0}
|
||||
@@ -448,8 +448,8 @@
|
||||
{:else}
|
||||
<div
|
||||
class="plus"
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24}
|
||||
>
|
||||
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} />
|
||||
</div>
|
||||
@@ -459,15 +459,15 @@
|
||||
{#if $nodeGraph.updateImportsExports.addImportExport}
|
||||
<div
|
||||
class="plus"
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
|
||||
>
|
||||
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
|
||||
</div>
|
||||
<div
|
||||
class="plus"
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
|
||||
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
|
||||
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
|
||||
>
|
||||
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryExport()} />
|
||||
</div>
|
||||
@@ -475,16 +475,16 @@
|
||||
|
||||
{#if $nodeGraph.reorderImportIndex !== undefined}
|
||||
{@const position = {
|
||||
x: Number($nodeGraph.updateImportsExports.importPosition.x),
|
||||
y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24,
|
||||
x: Number($nodeGraph.updateImportsExports.importPosition[0]),
|
||||
y: Number($nodeGraph.updateImportsExports.importPosition[1]) + Number($nodeGraph.reorderImportIndex) * 24,
|
||||
}}
|
||||
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24}></div>
|
||||
{/if}
|
||||
|
||||
{#if $nodeGraph.reorderExportIndex !== undefined}
|
||||
{@const position = {
|
||||
x: Number($nodeGraph.updateImportsExports.exportPosition.x),
|
||||
y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24,
|
||||
x: Number($nodeGraph.updateImportsExports.exportPosition[0]),
|
||||
y: Number($nodeGraph.updateImportsExports.exportPosition[1]) + Number($nodeGraph.reorderExportIndex) * 24,
|
||||
}}
|
||||
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24}></div>
|
||||
{/if}
|
||||
@@ -510,8 +510,8 @@
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={!node.visible}
|
||||
class:locked={node.locked}
|
||||
style:--offset-left={node.position?.x || 0}
|
||||
style:--offset-top={node.position?.y || 0}
|
||||
style:--offset-left={node.position?.[0] || 0}
|
||||
style:--offset-top={node.position?.[1] || 0}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
@@ -519,7 +519,7 @@
|
||||
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
|
||||
data-tooltip-label={nodeNameTooltipLabel(node)}
|
||||
data-tooltip-description={`
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
|
||||
`.trim()}
|
||||
data-node={node.id}
|
||||
>
|
||||
@@ -675,14 +675,14 @@
|
||||
class:selected={$nodeGraph.selected.includes(node.id)}
|
||||
class:previewed={node.previewed}
|
||||
class:disabled={!node.visible}
|
||||
style:--offset-left={node.position?.x || 0}
|
||||
style:--offset-top={node.position?.y || 0}
|
||||
style:--offset-left={node.position?.[0] || 0}
|
||||
style:--offset-top={node.position?.[1] || 0}
|
||||
style:--clip-path-id={`url(#${clipPathId})`}
|
||||
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
|
||||
data-tooltip-label={nodeNameTooltipLabel(node)}
|
||||
data-tooltip-description={`
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
|
||||
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
|
||||
`.trim()}
|
||||
data-node={node.id}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type Layout, isWidgetTable, type LayoutTarget } from "@graphite/messages";
|
||||
import type { Layout, LayoutTarget } from "@graphite/messages";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetTable, isWidgetSection } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { isWidgetSpanRow, isWidgetSection, type WidgetSection as WidgetSectionFromJsMessages, type LayoutTarget } from "@graphite/messages";
|
||||
import type { WidgetSection as WidgetSectionData, LayoutTarget } from "@graphite/messages";
|
||||
import { isWidgetSpanRow, isWidgetSection } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetSectionFromJsMessages;
|
||||
export let widgetData: WidgetSectionData;
|
||||
export let layoutTarget: LayoutTarget;
|
||||
|
||||
let className = "";
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
|
||||
import { parseFillChoice } from "@graphite/utility-functions/colors";
|
||||
import { debouncer } from "@graphite/utility-functions/debounce";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, createLayoutGroup } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
||||
@@ -97,6 +98,7 @@
|
||||
component: ColorInput,
|
||||
getProps: (props: WidgetTypes["ColorInput"], index) => ({
|
||||
...exclude(props),
|
||||
value: parseFillChoice(props.value),
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||
@@ -191,6 +193,7 @@
|
||||
getProps: (props: WidgetTypes["PopoverButton"]) => ({
|
||||
...exclude(props),
|
||||
layoutTarget,
|
||||
popoverLayout: props.popoverLayout.map(createLayoutGroup),
|
||||
}),
|
||||
},
|
||||
RadioInput: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { type LayoutTarget, type WidgetTable as WidgetTableFromJsMessages } from "@graphite/messages";
|
||||
import type { LayoutTarget, WidgetTable as WidgetTableData } from "@graphite/messages";
|
||||
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetTableFromJsMessages;
|
||||
export let widgetData: WidgetTableData;
|
||||
export let layoutTarget: LayoutTarget;
|
||||
export let unstyled = false;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type IconName, type IconSize } from "@graphite/icons";
|
||||
import type { IconName, IconSize } from "@graphite/icons";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type IconName, type PopoverButtonStyle } from "@graphite/icons";
|
||||
import type { IconName, PopoverButtonStyle } from "@graphite/icons";
|
||||
|
||||
import type { MenuDirection, ActionShortcut, Layout, LayoutTarget } from "@graphite/messages";
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages";
|
||||
import { type Color, contrastingOutlineFactor, isColor, isGradient, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/messages";
|
||||
import type { Color } from "@graphite/messages";
|
||||
import { contrastingOutlineFactor, isColor, isGradient, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { type RadioEntryData } from "@graphite/messages";
|
||||
import type { RadioEntryData } from "@graphite/messages";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
|
||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { type Color, type Gradient, createColor, colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/messages";
|
||||
import type { Color, Gradient } from "@graphite/messages";
|
||||
import { createColor, colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type Color, isColor, colorToRgbaCSS } from "@graphite/messages";
|
||||
import type { Color } from "@graphite/messages";
|
||||
import { isColor, colorToRgbaCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type IconName, ICONS, ICON_SVG_STRINGS } from "@graphite/icons";
|
||||
import { ICONS, ICON_SVG_STRINGS } from "@graphite/icons";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type SeparatorDirection, type SeparatorStyle } from "@graphite/messages";
|
||||
import type { SeparatorDirection, SeparatorStyle } from "@graphite/messages";
|
||||
|
||||
// Content
|
||||
export let direction: SeparatorDirection = "Horizontal";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout, UpdateStatusBarHintsLayout, UpdateStatusBarInfoLayout } from "@graphite/messages";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
@@ -15,11 +15,11 @@
|
||||
let statusBarInfoLayout: Layout = [];
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateStatusBarHintsLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("StatusBarHints", (data) => {
|
||||
patchLayout(statusBarHintsLayout, data);
|
||||
statusBarHintsLayout = statusBarHintsLayout;
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateStatusBarInfoLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("StatusBarInfo", (data) => {
|
||||
patchLayout(statusBarInfoLayout, data);
|
||||
statusBarInfoLayout = statusBarInfoLayout;
|
||||
});
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
@@ -26,7 +26,7 @@
|
||||
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("MenuBar", (data) => {
|
||||
patchLayout(menuBarLayout, data);
|
||||
menuBarLayout = menuBarLayout;
|
||||
});
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { type JsMessageType } from "@graphite/messages";
|
||||
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import type { FrontendMessages } from "@graphite/messages";
|
||||
import { createSubscriptionRouter } from "@graphite/subscription-router";
|
||||
import type { SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
// TODO: Remove `raw`, split out `subscriptions`, and unwrap the remaining `handle` so `EditorHandle` can replace `Editor` and then it can also be renamed to `Editor` to fully remove `EditorHandle`.
|
||||
@@ -45,10 +46,9 @@ export function createEditor(): Editor {
|
||||
const randomSeed = BigInt(randomSeedFloat);
|
||||
|
||||
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
|
||||
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: JsMessageType, messageData: Record<string, unknown>) => {
|
||||
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
|
||||
// This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle`
|
||||
// We pass along the first two arguments then add our own `raw` and `handle` context for the last two arguments
|
||||
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
|
||||
subscriptions.handleFrontendMessage(messageType, messageData);
|
||||
});
|
||||
|
||||
// Subscriptions: allows subscribing to messages in JS that are sent from the Wasm backend
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
export function createClipboardManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(TriggerClipboardWrite, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
|
||||
// If the Clipboard API is supported in the browser, copy text to the clipboard
|
||||
navigator.clipboard?.writeText?.(data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSelectionRead, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
|
||||
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSelectionWrite, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
|
||||
insertAtCaret(data.content);
|
||||
});
|
||||
}
|
||||
@@ -44,7 +43,7 @@ function readAtCaret(cut: boolean): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedText = selection.toString();
|
||||
const selectedText = String(selection);
|
||||
if (!selectedText) return undefined;
|
||||
|
||||
if (cut) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontCatalogLoad, TriggerFontDataLoad } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[];
|
||||
|
||||
@@ -7,7 +6,7 @@ const FONT_LIST_API = "https://api.graphite.art/font-list";
|
||||
|
||||
export function createFontsManager(editor: Editor) {
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontCatalogLoad, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
|
||||
const response = await fetch(FONT_LIST_API);
|
||||
const fontListResponse = (await response.json()) as { items: ApiResponse };
|
||||
const fontListData = fontListResponse.items;
|
||||
@@ -26,7 +25,7 @@ export function createFontsManager(editor: Editor) {
|
||||
editor.handle.onFontCatalogLoad(catalog);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontDataLoad, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
|
||||
const { fontFamily, fontStyle } = data.font;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerVisitLink } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
export function createHyperlinkManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(TriggerVisitLink, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
|
||||
window.open(data.url, "_blank");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerClipboardRead, WindowPointerLockMove } from "@graphite/messages";
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import { type DocumentState } from "@graphite/state-providers/document";
|
||||
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform";
|
||||
@@ -392,7 +391,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
// Frontend message subscriptions
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerClipboardRead, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardRead", async () => {
|
||||
// In the try block, attempt to read from the Clipboard API, which may not have permission and may not be supported in all browsers
|
||||
// In the catch block, explain to the user why the paste failed and how to fix or work around the problem
|
||||
try {
|
||||
@@ -487,7 +486,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
});
|
||||
|
||||
// Pointer lock movement events on desktop
|
||||
editor.subscriptions.subscribeJsMessage(WindowPointerLockMove, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => {
|
||||
const event = new CustomEvent("pointerlockmove", { detail: data });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
export function createLocalizationManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate", (data) => {
|
||||
const localized = localizeTimestamp(data.commitDate);
|
||||
editor.handle.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { DisplayDialogPanic } from "@graphite/messages";
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
|
||||
export function createPanicManager(editor: Editor, dialogState: DialogState) {
|
||||
// Code panic dialog and console error
|
||||
editor.subscriptions.subscribeJsMessage(DisplayDialogPanic, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => {
|
||||
// `Error.stackTraceLimit` is only available in V8/Chromium
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Error as any).stackTraceLimit = Infinity;
|
||||
@@ -68,7 +67,7 @@ export function githubUrl(panicDetails: string): string {
|
||||
if (value) url.searchParams.set(field, value);
|
||||
});
|
||||
|
||||
return url.toString();
|
||||
return String(url);
|
||||
};
|
||||
|
||||
let urlString = buildUrl(true);
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { createStore, del, get, set, update } from "idb-keyval";
|
||||
import { get as getFromStore } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import {
|
||||
TriggerPersistenceWriteDocument,
|
||||
TriggerPersistenceRemoveDocument,
|
||||
TriggerSavePreferences,
|
||||
TriggerLoadPreferences,
|
||||
TriggerLoadFirstAutoSaveDocument,
|
||||
TriggerLoadRestAutoSaveDocuments,
|
||||
TriggerSaveActiveDocument,
|
||||
TriggerOpenLaunchDocuments,
|
||||
} from "@graphite/messages";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { FrontendMessages } from "@graphite/messages";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
||||
type TriggerPersistenceWriteDocument = FrontendMessages["TriggerPersistenceWriteDocument"];
|
||||
type TriggerSavePreferences = FrontendMessages["TriggerSavePreferences"];
|
||||
|
||||
const graphiteStore = createStore("graphite", "store");
|
||||
|
||||
@@ -33,14 +27,14 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
"documents",
|
||||
(old) => {
|
||||
const documents = old || {};
|
||||
documents[autoSaveDocument.documentId] = autoSaveDocument;
|
||||
documents[String(autoSaveDocument.documentId)] = autoSaveDocument;
|
||||
return documents;
|
||||
},
|
||||
graphiteStore,
|
||||
);
|
||||
|
||||
await storeDocumentOrder();
|
||||
await storeCurrentDocumentId(autoSaveDocument.documentId);
|
||||
await storeCurrentDocumentId(String(autoSaveDocument.documentId));
|
||||
}
|
||||
|
||||
async function removeDocument(id: string) {
|
||||
@@ -79,35 +73,56 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
|
||||
async function loadFirstDocument() {
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
if (previouslySavedDocuments) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Object.values(previouslySavedDocuments).forEach((doc: any) => {
|
||||
if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId);
|
||||
});
|
||||
}
|
||||
|
||||
const documentOrder = await get<string[]>("documents_tab_order", graphiteStore);
|
||||
const currentDocumentId = await get<string>("current_document_id", graphiteStore);
|
||||
const currentDocumentIdString = await get<string>("current_document_id", graphiteStore);
|
||||
const currentDocumentId = currentDocumentIdString ? BigInt(currentDocumentIdString) : undefined;
|
||||
if (!previouslySavedDocuments || !documentOrder) return;
|
||||
|
||||
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
|
||||
|
||||
if (currentDocumentId && currentDocumentId in previouslySavedDocuments) {
|
||||
const doc = previouslySavedDocuments[currentDocumentId];
|
||||
editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false);
|
||||
editor.handle.selectDocument(BigInt(currentDocumentId));
|
||||
if (currentDocumentId !== undefined && String(currentDocumentId) in previouslySavedDocuments) {
|
||||
const doc = previouslySavedDocuments[String(currentDocumentId)];
|
||||
editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
|
||||
editor.handle.selectDocument(currentDocumentId);
|
||||
} else {
|
||||
const len = orderedSavedDocuments.length;
|
||||
if (len > 0) {
|
||||
const doc = orderedSavedDocuments[len - 1];
|
||||
editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false);
|
||||
editor.handle.selectDocument(BigInt(doc.documentId));
|
||||
editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
|
||||
editor.handle.selectDocument(doc.documentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRestDocuments() {
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
if (previouslySavedDocuments) {
|
||||
Object.values(previouslySavedDocuments).forEach((doc) => {
|
||||
if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId);
|
||||
});
|
||||
}
|
||||
|
||||
const documentOrder = await get<string[]>("documents_tab_order", graphiteStore);
|
||||
const currentDocumentId = await get<string>("current_document_id", graphiteStore);
|
||||
const currentDocumentIdString = await get<string>("current_document_id", graphiteStore);
|
||||
const currentDocumentId = currentDocumentIdString ? BigInt(currentDocumentIdString) : undefined;
|
||||
if (!previouslySavedDocuments || !documentOrder) return;
|
||||
|
||||
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
|
||||
|
||||
if (currentDocumentId) {
|
||||
if (currentDocumentId !== undefined) {
|
||||
const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.documentId === currentDocumentId);
|
||||
const beforeCurrentIndex = currentIndex - 1;
|
||||
const afterCurrentIndex = currentIndex + 1;
|
||||
@@ -115,28 +130,25 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
for (let i = beforeCurrentIndex; i >= 0; i--) {
|
||||
const { documentId, document, details } = orderedSavedDocuments[i];
|
||||
const { name, isSaved } = details;
|
||||
editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, true);
|
||||
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, true);
|
||||
}
|
||||
for (let i = afterCurrentIndex; i < orderedSavedDocuments.length; i++) {
|
||||
const { documentId, document, details } = orderedSavedDocuments[i];
|
||||
const { name, isSaved } = details;
|
||||
editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, false);
|
||||
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, false);
|
||||
}
|
||||
|
||||
editor.handle.selectDocument(BigInt(currentDocumentId));
|
||||
editor.handle.selectDocument(currentDocumentId);
|
||||
} else {
|
||||
const length = orderedSavedDocuments.length;
|
||||
|
||||
for (let i = length - 2; i >= 0; i--) {
|
||||
const { documentId, document, details } = orderedSavedDocuments[i];
|
||||
const { name, isSaved } = details;
|
||||
editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, true);
|
||||
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, true);
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
const id = orderedSavedDocuments[length - 1].documentId;
|
||||
editor.handle.selectDocument(BigInt(id));
|
||||
}
|
||||
if (length > 0) editor.handle.selectDocument(orderedSavedDocuments[length - 1].documentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,30 +166,39 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
// FRONTEND MESSAGE SUBSCRIPTIONS
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSavePreferences", async (data) => {
|
||||
await savePreferences(data.preferences);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerLoadPreferences", async () => {
|
||||
await loadPreferences();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerPersistenceWriteDocument, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
|
||||
await storeDocument(data);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerPersistenceRemoveDocument, async (data) => {
|
||||
await removeDocument(data.documentId);
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => {
|
||||
await removeDocument(String(data.documentId));
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerLoadFirstAutoSaveDocument, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument", async () => {
|
||||
await loadFirstDocument();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerLoadRestAutoSaveDocuments, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments", async () => {
|
||||
await loadRestDocuments();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerOpenLaunchDocuments, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => {
|
||||
// TODO: Could be used to load documents from URL params or similar on launch
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSaveActiveDocument, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => {
|
||||
const documentId = String(data.documentId);
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
if (previouslySavedDocuments) {
|
||||
Object.values(previouslySavedDocuments).forEach((doc) => {
|
||||
if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId);
|
||||
});
|
||||
}
|
||||
|
||||
if (!previouslySavedDocuments) return;
|
||||
if (documentId in previouslySavedDocuments) {
|
||||
await storeCurrentDocumentId(documentId);
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// This file is the browser's entry point for the JS bundle
|
||||
|
||||
// `reflect-metadata` allows for runtime reflection of types in JavaScript.
|
||||
// It is needed for class-transformer to work and is imported as a side effect.
|
||||
// The library replaces the Reflect API on the window to support more features.
|
||||
import "reflect-metadata";
|
||||
import { mount } from "svelte";
|
||||
|
||||
import App from "@graphite/App.svelte";
|
||||
|
||||
+509
-1452
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen, UpdateUIScale } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { AppWindowPlatform } from "@graphite/messages";
|
||||
|
||||
export function createAppWindowState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
@@ -13,31 +13,31 @@ export function createAppWindowState(editor: Editor) {
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePlatform, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdatePlatform", (data) => {
|
||||
update((state) => {
|
||||
state.platform = data.platform;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMaximized, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateMaximized", (data) => {
|
||||
update((state) => {
|
||||
state.maximized = data.maximized;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateFullscreen, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateFullscreen", (data) => {
|
||||
update((state) => {
|
||||
state.fullscreen = data.fullscreen;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateViewportHolePunch, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateViewportHolePunch", (data) => {
|
||||
update((state) => {
|
||||
state.viewportHolePunch = data.active;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateUIScale, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateUIScale", (data) => {
|
||||
update((state) => {
|
||||
state.uiScale = data.scale;
|
||||
return state;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { type IconName } from "@graphite/icons";
|
||||
import { DisplayDialog, DialogClose, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchLayout, TriggerDisplayThirdPartyLicensesDialog } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
export function createDialogState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
@@ -45,7 +45,7 @@ export function createDialogState(editor: Editor) {
|
||||
}
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(DisplayDialog, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayDialog", (data) => {
|
||||
update((state) => {
|
||||
state.visible = true;
|
||||
|
||||
@@ -55,30 +55,30 @@ export function createDialogState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("DialogButtons", (data) => {
|
||||
update((state) => {
|
||||
patchLayout(state.buttons, data);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn1, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("DialogColumn1", (data) => {
|
||||
update((state) => {
|
||||
patchLayout(state.column1, data);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn2, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("DialogColumn2", (data) => {
|
||||
update((state) => {
|
||||
patchLayout(state.column2, data);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DialogClose, dismissDialog);
|
||||
editor.subscriptions.subscribeFrontendMessage("DialogClose", dismissDialog);
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerDisplayThirdPartyLicensesDialog, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerDisplayThirdPartyLicensesDialog", async () => {
|
||||
const BACKUP_URL = "https://editor.graphite.art/third-party-licenses.txt";
|
||||
let licenseText = `Content was not able to load. Please check your network connection and try again.\n\nOr visit ${BACKUP_URL} for the license notices.`;
|
||||
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
|
||||
import {
|
||||
patchLayout,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateWorkingColorsLayout,
|
||||
UpdateNodeGraphControlBarLayout,
|
||||
UpdateGraphViewOverlay,
|
||||
UpdateGraphFadeArtwork,
|
||||
} from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Layout } from "@graphite/messages";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
export function createDocumentState(editor: Editor) {
|
||||
const state = writable({
|
||||
@@ -30,13 +20,13 @@ export function createDocumentState(editor: Editor) {
|
||||
const { subscribe, update } = state;
|
||||
|
||||
// Update layouts
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGraphFadeArtwork, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateGraphFadeArtwork", (data) => {
|
||||
update((state) => {
|
||||
state.fadeArtwork = data.percentage;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, async (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("ToolOptions", async (data) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
@@ -44,7 +34,7 @@ export function createDocumentState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, async (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("DocumentBar", async (data) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
@@ -52,7 +42,7 @@ export function createDocumentState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, async (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("ToolShelf", async (data) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
@@ -60,7 +50,7 @@ export function createDocumentState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWorkingColorsLayout, async (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("WorkingColors", async (data) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
@@ -68,7 +58,7 @@ export function createDocumentState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphControlBarLayout, (data) => {
|
||||
editor.subscriptions.subscribeLayoutUpdate("NodeGraphControlBar", (data) => {
|
||||
update((state) => {
|
||||
patchLayout(state.nodeGraphControlBarLayout, data);
|
||||
return state;
|
||||
@@ -76,7 +66,7 @@ export function createDocumentState(editor: Editor) {
|
||||
});
|
||||
|
||||
// Show or hide the graph view overlay
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGraphViewOverlay, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateGraphViewOverlay", (data) => {
|
||||
update((state) => {
|
||||
state.graphViewOverlayOpen = data.open;
|
||||
return state;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { WindowFullscreen } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
export function createFullscreenState(editor: Editor) {
|
||||
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard
|
||||
@@ -51,7 +50,7 @@ export function createFullscreenState(editor: Editor) {
|
||||
});
|
||||
}
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(WindowFullscreen, () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("WindowFullscreen", () => {
|
||||
toggleFullscreen();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { NodeGraphError } from "@graphite/messages";
|
||||
import {
|
||||
type Box,
|
||||
type FrontendClickTargets,
|
||||
type ContextMenuInformation,
|
||||
type FrontendNode,
|
||||
type FrontendNodeType,
|
||||
type WirePath,
|
||||
ClearAllNodeGraphWires,
|
||||
SendUIMetadata,
|
||||
UpdateBox,
|
||||
UpdateClickTargets,
|
||||
UpdateContextMenuInformation,
|
||||
UpdateInSelectedNetwork,
|
||||
UpdateImportReorderIndex,
|
||||
UpdateExportReorderIndex,
|
||||
UpdateImportsExports,
|
||||
UpdateLayerWidths,
|
||||
UpdateNodeGraphNodes,
|
||||
UpdateVisibleNodes,
|
||||
UpdateNodeGraphWires,
|
||||
UpdateNodeGraphSelection,
|
||||
UpdateNodeGraphTransform,
|
||||
UpdateNodeThumbnail,
|
||||
UpdateWirePathInProgress,
|
||||
UpdateNodeGraphErrorDiagnostic,
|
||||
} from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { NodeGraphError, Box, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages";
|
||||
|
||||
type UpdateImportsExports = FrontendMessages["UpdateImportsExports"];
|
||||
|
||||
export function createNodeGraphState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
@@ -62,56 +38,56 @@ export function createNodeGraphState(editor: Editor) {
|
||||
}
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(SendUIMetadata, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("SendUIMetadata", (data) => {
|
||||
update((state) => {
|
||||
state.nodeDescriptions = data.nodeDescriptions;
|
||||
state.nodeDescriptions = new Map(data.nodeDescriptions);
|
||||
state.nodeTypes = data.nodeTypes;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateBox, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateBox", (data) => {
|
||||
update((state) => {
|
||||
state.box = data.box;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateClickTargets", (data) => {
|
||||
update((state) => {
|
||||
state.clickTargets = data.clickTargets;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateContextMenuInformation, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateContextMenuInformation", (data) => {
|
||||
update((state) => {
|
||||
state.contextMenuInformation = data.contextMenuInformation;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImportReorderIndex, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateImportReorderIndex", (data) => {
|
||||
update((state) => {
|
||||
state.reorderImportIndex = data.importIndex;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateExportReorderIndex, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateExportReorderIndex", (data) => {
|
||||
update((state) => {
|
||||
state.reorderExportIndex = data.exportIndex;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateImportsExports", (data) => {
|
||||
update((state) => {
|
||||
state.updateImportsExports = data;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateInSelectedNetwork", (data) => {
|
||||
update((state) => {
|
||||
state.inSelectedNetwork = data.inSelectedNetwork;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateLayerWidths", (data) => {
|
||||
update((state) => {
|
||||
state.layerWidths = data.layerWidths;
|
||||
state.chainWidths = data.chainWidths;
|
||||
@@ -119,7 +95,7 @@ export function createNodeGraphState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphNodes", (data) => {
|
||||
update((state) => {
|
||||
state.nodes.clear();
|
||||
data.nodes.forEach((node) => {
|
||||
@@ -128,19 +104,19 @@ export function createNodeGraphState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphErrorDiagnostic, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic", (data) => {
|
||||
update((state) => {
|
||||
state.error = data.error;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateVisibleNodes", (data) => {
|
||||
update((state) => {
|
||||
state.visibleNodes = new Set<bigint>(data.nodes);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphWires, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphWires", (data) => {
|
||||
update((state) => {
|
||||
data.wires.forEach((wireUpdate) => {
|
||||
let inputMap = state.wires.get(wireUpdate.id);
|
||||
@@ -158,31 +134,31 @@ export function createNodeGraphState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(ClearAllNodeGraphWires, () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("ClearAllNodeGraphWires", () => {
|
||||
update((state) => {
|
||||
state.wires.clear();
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphSelection", (data) => {
|
||||
update((state) => {
|
||||
state.selected = data.selected;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphTransform, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => {
|
||||
update((state) => {
|
||||
state.transform = data.transform;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeThumbnail, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateNodeThumbnail", (data) => {
|
||||
update((state) => {
|
||||
state.thumbnails.set(data.id, data.value);
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWirePathInProgress, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateWirePathInProgress", (data) => {
|
||||
update((state) => {
|
||||
state.wirePathInProgress = data.wirePath;
|
||||
return state;
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { OpenDocument } from "@graphite/messages";
|
||||
import {
|
||||
TriggerFetchAndOpenDocument,
|
||||
TriggerSaveDocument,
|
||||
TriggerExportImage,
|
||||
TriggerSaveFile,
|
||||
TriggerImport,
|
||||
TriggerOpen,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateDataPanelState,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateLayersPanelState,
|
||||
} from "@graphite/messages";
|
||||
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
||||
import { rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
||||
|
||||
@@ -29,13 +16,13 @@ export function createPortfolioState(editor: Editor) {
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdateOpenDocumentsList, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateOpenDocumentsList", (data) => {
|
||||
update((state) => {
|
||||
state.documents = data.openDocuments;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateActiveDocument, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {
|
||||
update((state) => {
|
||||
// Assume we receive a correct document id
|
||||
const activeId = state.documents.findIndex((doc) => doc.id === data.documentId);
|
||||
@@ -43,7 +30,7 @@ export function createPortfolioState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerFetchAndOpenDocument", async (data) => {
|
||||
try {
|
||||
const url = new URL(`demo-artwork/${data.filename}`, document.location.href);
|
||||
const response = await fetch(url);
|
||||
@@ -55,22 +42,22 @@ export function createPortfolioState(editor: Editor) {
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerOpen", async () => {
|
||||
const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data");
|
||||
editor.handle.openFile(data.filename, data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerImport", async () => {
|
||||
// TODO: Use the same `accept` string as in the `TriggerOpen` handler once importing Graphite documents as nodes is supported
|
||||
const data = await upload("image/*", "data");
|
||||
editor.handle.importFile(data.filename, data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSaveDocument", (data) => {
|
||||
downloadFile(data.name, data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSaveFile, (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSaveFile", (data) => {
|
||||
downloadFile(data.name, data.content);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerExportImage, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerExportImage", async (data) => {
|
||||
const { svg, name, mime, size } = data;
|
||||
|
||||
// Fill the canvas with white if it'll be a JPEG (which does not support transparency and defaults to black)
|
||||
@@ -78,7 +65,7 @@ export function createPortfolioState(editor: Editor) {
|
||||
|
||||
// Rasterize the SVG to an image file
|
||||
try {
|
||||
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
|
||||
const blob = await rasterizeSVG(svg, size[0], size[1], mime, backgroundColor);
|
||||
|
||||
// Have the browser download the file to the user's disk
|
||||
downloadFileBlob(name, blob);
|
||||
@@ -86,19 +73,19 @@ export function createPortfolioState(editor: Editor) {
|
||||
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDataPanelState, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateDataPanelState", async (data) => {
|
||||
update((state) => {
|
||||
state.dataPanelOpen = data.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdatePropertiesPanelState", async (data) => {
|
||||
update((state) => {
|
||||
state.propertiesPanelOpen = data.open;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("UpdateLayersPanelState", async (data) => {
|
||||
update((state) => {
|
||||
state.layersPanelOpen = data.open;
|
||||
return state;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { SendShortcutAltClick, SendShortcutFullscreen, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
const SHOW_TOOLTIP_DELAY_MS = 500;
|
||||
@@ -65,19 +65,19 @@ export function createTooltipState(editor: Editor) {
|
||||
document.addEventListener("keydown", closeTooltip);
|
||||
document.addEventListener("wheel", closeTooltip);
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutShiftClick, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("SendShortcutShiftClick", async (data) => {
|
||||
update((state) => {
|
||||
state.shiftClickShortcut = data.shortcut;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("SendShortcutAltClick", async (data) => {
|
||||
update((state) => {
|
||||
state.altClickShortcut = data.shortcut;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(SendShortcutFullscreen, async (data) => {
|
||||
editor.subscriptions.subscribeFrontendMessage("SendShortcutFullscreen", async (data) => {
|
||||
update((state) => {
|
||||
state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut;
|
||||
return state;
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import type { FrontendMessages, LayoutTarget, WidgetDiff } from "@graphite/messages";
|
||||
import { parseWidgetDiffs } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import { type EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/messages";
|
||||
|
||||
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
|
||||
// Don't know a better way of typing this since it can be any subclass of JsMessage
|
||||
// The functions interacting with this map are strongly typed though around JsMessage
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type JsMessageCallbackMap = Record<string, JsMessageCallback<any> | undefined>;
|
||||
type FrontendMessageCallbacks = Record<string, ((messageData: any) => void) | undefined>;
|
||||
|
||||
export function createSubscriptionRouter() {
|
||||
const subscriptions: JsMessageCallbackMap = {};
|
||||
const subscriptions: FrontendMessageCallbacks = {};
|
||||
const layoutCallbacks: Partial<Record<LayoutTarget, (diffs: WidgetDiff[]) => void>> = {};
|
||||
|
||||
const subscribeJsMessage = <T extends JsMessage, Args extends unknown[]>(messageType: new (...args: Args) => T, callback: JsMessageCallback<T>) => {
|
||||
subscriptions[messageType.name] = callback;
|
||||
const subscribeFrontendMessage = <T extends keyof FrontendMessages>(messageType: T, callback: (data: FrontendMessages[T]) => void) => {
|
||||
subscriptions[messageType] = callback;
|
||||
};
|
||||
|
||||
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => {
|
||||
delete subscriptions[messageType.name];
|
||||
const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => {
|
||||
delete subscriptions[messageType];
|
||||
};
|
||||
|
||||
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
|
||||
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
|
||||
const messageMaker = messageMakers[messageType];
|
||||
if (!messageMaker) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` +
|
||||
"(Perhaps this message parser isn't exported in `messageMakers` at the bottom of `messages.ts`.)",
|
||||
);
|
||||
return;
|
||||
const subscribeLayoutUpdate = (target: LayoutTarget, callback: (diffs: WidgetDiff[]) => void) => {
|
||||
layoutCallbacks[target] = callback;
|
||||
};
|
||||
|
||||
const unsubscribeLayoutUpdate = (target: LayoutTarget) => {
|
||||
delete layoutCallbacks[target];
|
||||
};
|
||||
|
||||
const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
|
||||
// Messages with non-empty data are provided by Serde JSON as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by Serde JSON as a string with the message name, like: "NameOfThisMessage"
|
||||
// Here we extract the payload object or use an empty object depending on the situation.
|
||||
const message = messageData[messageType] || {};
|
||||
|
||||
// Resolve the callback lookup and the data to pass, depending on whether this is a layout update or a regular message.
|
||||
// UpdateLayout messages are dispatched to layout-specific callbacks based on the layout target.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let getCallback: () => ((data: any) => void) | undefined;
|
||||
let callbackData: unknown;
|
||||
let errorLabel: string;
|
||||
if (messageType === "UpdateLayout") {
|
||||
const { layoutTarget, diff } = message as FrontendMessages["UpdateLayout"];
|
||||
getCallback = () => layoutCallbacks[layoutTarget];
|
||||
callbackData = parseWidgetDiffs(diff);
|
||||
errorLabel = `UpdateLayout for layout target "${layoutTarget}"`;
|
||||
} else {
|
||||
getCallback = () => subscriptions[messageType];
|
||||
callbackData = message;
|
||||
errorLabel = messageType;
|
||||
}
|
||||
|
||||
// Checks if the provided `messageMaker` is a class extending `JsMessage`. All classes inheriting from `JsMessage` will have a static readonly `jsMessageMarker` which is `true`.
|
||||
const isJsMessageMaker = (fn: typeof messageMaker): fn is typeof JsMessage => "jsMessageMarker" in fn;
|
||||
const messageIsClass = isJsMessageMaker(messageMaker);
|
||||
|
||||
// Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage"
|
||||
// Here we extract the payload object or use an empty object depending on the situation.
|
||||
const unwrappedMessageData = messageData[messageType] || {};
|
||||
|
||||
// Converts to a `JsMessage` object by turning the JSON message data into an instance of the message class, either automatically or by calling the function that builds it.
|
||||
// If the `messageMaker` is a `JsMessage` class then we use the class-transformer library's `plainToInstance` function in order to convert the JSON data into the destination class.
|
||||
// If it is not a `JsMessage` then it should be a custom function that creates a JsMessage from a JSON, so we call the function itself with the raw JSON as an argument.
|
||||
// The resulting `message` is an instance of a class that extends `JsMessage`.
|
||||
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, handle);
|
||||
|
||||
// If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
|
||||
// The frontend should always have a callback for all messages, but due to message ordering, we might have to delay a few stack frames until we do.
|
||||
// Try to execute the callback. Due to message ordering, the callback may not be registered yet,
|
||||
// so we retry a few times on the next stack frame to give onMount a chance to run.
|
||||
let retries = 0;
|
||||
const callCallback = () => {
|
||||
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
|
||||
const callback = subscriptions[message.constructor.name];
|
||||
const callback = getCallback();
|
||||
|
||||
// Attempt to call the callback, but try again several times on the next stack frame if it is not yet registered due to message ordering.
|
||||
if (callback) {
|
||||
callback(message);
|
||||
callback(callbackData);
|
||||
} else if (retries <= 3) {
|
||||
retries += 1;
|
||||
setTimeout(callCallback, 0);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
|
||||
console.error(`Received a frontend message of type "${errorLabel}" but no handler was registered for it from the client.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,9 +68,11 @@ export function createSubscriptionRouter() {
|
||||
};
|
||||
|
||||
return {
|
||||
subscribeJsMessage,
|
||||
unsubscribeJsMessage,
|
||||
handleJsMessage,
|
||||
subscribeFrontendMessage,
|
||||
unsubscribeFrontendMessage,
|
||||
subscribeLayoutUpdate,
|
||||
unsubscribeLayoutUpdate,
|
||||
handleFrontendMessage,
|
||||
};
|
||||
}
|
||||
export type SubscriptionRouter = ReturnType<typeof createSubscriptionRouter>;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { sampleInterpolatedGradient } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Color, FillChoice, Gradient } from "@graphite/messages";
|
||||
|
||||
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
||||
export type HSV = { h: number; s: number; v: number };
|
||||
export type RGB = { r: number; g: number; b: number };
|
||||
|
||||
// COLOR FACTORY FUNCTIONS
|
||||
|
||||
export function createColor(red: number, green: number, blue: number, alpha: number): Color {
|
||||
return { red, green, blue, alpha, none: false };
|
||||
}
|
||||
|
||||
export function createNoneColor(): Color {
|
||||
return { red: 0, green: 0, blue: 0, alpha: 1, none: true };
|
||||
}
|
||||
|
||||
export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color {
|
||||
const convert = (n: number): number => {
|
||||
const k = (n + h * 6) % 6;
|
||||
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
||||
};
|
||||
|
||||
return { red: convert(5), green: convert(3), blue: convert(1), alpha: a, none: false };
|
||||
}
|
||||
|
||||
// COLOR UTILITY FUNCTIONS
|
||||
|
||||
export function isColor(value: unknown): value is Color {
|
||||
return typeof value === "object" && value !== null && "red" in value;
|
||||
}
|
||||
|
||||
export function colorFromCSS(colorCode: string): Color | undefined {
|
||||
// Allow single-digit hex value inputs
|
||||
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", { willReadFrequently: true });
|
||||
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;
|
||||
|
||||
// Invalid color
|
||||
if (comparisonA !== comparisonB) {
|
||||
// If this color code didn't start with a #, add it and try again
|
||||
if (colorValue.trim().charAt(0) !== "#") return colorFromCSS(`#${colorValue.trim()}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
context.fillRect(0, 0, 1, 1);
|
||||
|
||||
const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data];
|
||||
return createColor(r / 255, g / 255, b / 255, a / 255);
|
||||
}
|
||||
|
||||
export function colorEquals(c1: Color, c2: Color): boolean {
|
||||
if (c1.none !== c2.none) return false;
|
||||
if (c1.none && c2.none) return true;
|
||||
return Math.abs(c1.red - c2.red) < 1e-6 && Math.abs(c1.green - c2.green) < 1e-6 && Math.abs(c1.blue - c2.blue) < 1e-6 && Math.abs(c1.alpha - c2.alpha) < 1e-6;
|
||||
}
|
||||
|
||||
export function colorToHexNoAlpha(color: Color): string | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
const r = Math.round(color.red * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
const g = Math.round(color.green * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
const b = Math.round(color.blue * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
|
||||
export function colorToHexOptionalAlpha(color: Color): string | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
const hex = colorToHexNoAlpha(color);
|
||||
const a = Math.round(color.alpha * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return a === "ff" ? hex : `${hex}${a}`;
|
||||
}
|
||||
|
||||
export function colorToRgb255(color: Color): RGB | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
return {
|
||||
r: Math.round(color.red * 255),
|
||||
g: Math.round(color.green * 255),
|
||||
b: Math.round(color.blue * 255),
|
||||
};
|
||||
}
|
||||
|
||||
export function colorToRgbCSS(color: Color): string | undefined {
|
||||
const rgb = colorToRgb255(color);
|
||||
if (!rgb) return undefined;
|
||||
|
||||
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
||||
}
|
||||
|
||||
export function colorToRgbaCSS(color: Color): string | undefined {
|
||||
const rgb = colorToRgb255(color);
|
||||
if (!rgb) return undefined;
|
||||
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`;
|
||||
}
|
||||
|
||||
export function colorToHSV(color: Color): HSV | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
const { red: r, green: g, blue: b } = color;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
export function colorOpaque(color: Color): Color | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
return createColor(color.red, color.green, color.blue, 1);
|
||||
}
|
||||
|
||||
export function colorLuminance(color: Color): number | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
// Convert alpha into white
|
||||
const r = color.red * color.alpha + (1 - color.alpha);
|
||||
const g = color.green * color.alpha + (1 - color.alpha);
|
||||
const b = color.blue * color.alpha + (1 - color.alpha);
|
||||
|
||||
// https://stackoverflow.com/a/3943023/775283
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function colorContrastingColor(color: Color): "black" | "white" {
|
||||
if (color.none) return "black";
|
||||
|
||||
const luminance = colorLuminance(color);
|
||||
|
||||
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
}
|
||||
|
||||
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) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || createNoneColor());
|
||||
|
||||
const contrast = (color: Color): number => {
|
||||
const lum = colorLuminance(color) || 0;
|
||||
let rangeLuminance1 = colorLuminance(range1) || 0;
|
||||
let rangeLuminance2 = colorLuminance(range2) || 0;
|
||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||
|
||||
const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2);
|
||||
|
||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (colorToHSV(color)?.s || 0));
|
||||
};
|
||||
|
||||
if (isGradient(value)) {
|
||||
if (value.color.length === 0) return 0;
|
||||
|
||||
const first = contrast(value.color[0]);
|
||||
const last = contrast(value.color[value.color.length - 1]);
|
||||
|
||||
return Math.min(first, last);
|
||||
}
|
||||
|
||||
return contrast(value);
|
||||
}
|
||||
|
||||
// GRADIENT UTILITY FUNCTIONS
|
||||
|
||||
export function isGradient(value: unknown): value is Gradient {
|
||||
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value;
|
||||
}
|
||||
|
||||
export function gradientToLinearGradientCSS(gradient: Gradient): string {
|
||||
if (gradient.position.length === 1) {
|
||||
return `linear-gradient(to right, ${colorToHexOptionalAlpha(gradient.color[0])} 0%, ${colorToHexOptionalAlpha(gradient.color[0])} 100%)`;
|
||||
}
|
||||
|
||||
const pieces = sampleInterpolatedGradient(new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color, false);
|
||||
return `linear-gradient(to right, ${pieces})`;
|
||||
}
|
||||
|
||||
export function gradientFirstColor(gradient: Gradient): Color | undefined {
|
||||
return gradient.color[0];
|
||||
}
|
||||
|
||||
export function gradientLastColor(gradient: Gradient): Color | undefined {
|
||||
return gradient.color[gradient.color.length - 1];
|
||||
}
|
||||
|
||||
// FILL CHOICE UTILITY FUNCTIONS
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function parseFillChoice(value: any): FillChoice {
|
||||
if (isColor(value)) return value;
|
||||
if (isGradient(value)) return value;
|
||||
|
||||
const gradient: Gradient | undefined = value["Gradient"];
|
||||
if (gradient) {
|
||||
const color = gradient.color.map((c) => createColor(c.red, c.green, c.blue, c.alpha));
|
||||
return { ...gradient, color };
|
||||
}
|
||||
|
||||
const solid = value["Solid"];
|
||||
if (solid) return createColor(solid.red, solid.green, solid.blue, solid.alpha);
|
||||
|
||||
return createNoneColor();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
|
||||
export function downloadFileURL(filename: string, url: string) {
|
||||
|
||||
@@ -10,7 +10,7 @@ export function panicProxy<T extends object>(module: T): T {
|
||||
if (!isFunction) return targetValue;
|
||||
|
||||
// Special handling to wrap the return of a constructor in the proxy
|
||||
const isClass = isFunction && /^\s*class\s+/.test(targetValue.toString());
|
||||
const isClass = isFunction && /^\s*class\s+/.test(String(targetValue));
|
||||
if (isClass) {
|
||||
return function (...args: unknown[]): unknown {
|
||||
// All three of these comment lines are necessary to suppress errors at both compile time and while editing this file (@ts-expect-error doesn't work here while editing the file)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { Layout, LayoutGroup, UIItem, WidgetDiff, WidgetInstance, WidgetSection, WidgetSpanColumn, WidgetSpanRow, WidgetTable } from "@graphite/messages";
|
||||
|
||||
export function isWidgetSpanColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetSpanColumn {
|
||||
return Boolean((layoutColumn as WidgetSpanColumn)?.columnWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpanRow {
|
||||
return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetTable(layoutTable: LayoutGroup): layoutTable is WidgetTable {
|
||||
return Boolean((layoutTable as WidgetTable)?.tableWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection {
|
||||
return Boolean((layoutRow as WidgetSection)?.layout);
|
||||
}
|
||||
|
||||
/// Unwraps the Serde tagged enum `{ widgetId, widget: { Kind: props } }` into `{ widgetId, props: { kind, ...props } }`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function parseWidgetInstance(widgetInstance: any): WidgetInstance {
|
||||
const widgetId = widgetInstance.widgetId;
|
||||
|
||||
const kind = Object.keys(widgetInstance.widget)[0];
|
||||
const props = widgetInstance.widget[kind];
|
||||
props.kind = kind;
|
||||
|
||||
return { widgetId, props };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function parseWidgetDiffs(rawDiffs: any): WidgetDiff[] {
|
||||
return rawDiffs.map((diff: WidgetDiff) => {
|
||||
const { widgetPath, newValue } = diff;
|
||||
|
||||
if ("layout" in newValue) return { widgetPath, newValue: newValue.layout.map(createLayoutGroup) };
|
||||
if ("layoutGroup" in newValue) return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) };
|
||||
if ("widget" in newValue) return { widgetPath, newValue: parseWidgetInstance(newValue.widget) };
|
||||
|
||||
// This code should be unreachable
|
||||
throw new Error("DiffUpdate invalid");
|
||||
});
|
||||
}
|
||||
|
||||
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
|
||||
export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) {
|
||||
diffs.forEach((update) => {
|
||||
// Find the object where the diff applies to
|
||||
const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
|
||||
if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
|
||||
if (targetLayout && "props" in targetLayout && "widgetId" in targetLayout) {
|
||||
if (targetLayout.props.kind === "PopoverButton" && "popoverLayout" in targetLayout.props && targetLayout.props.popoverLayout) {
|
||||
targetLayout.props.popoverLayout = targetLayout.props.popoverLayout.map(createLayoutGroup);
|
||||
return targetLayout.props.popoverLayout[index];
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Tried to index widget");
|
||||
return targetLayout;
|
||||
}
|
||||
|
||||
return targetLayout?.[index];
|
||||
}, layout as UIItem);
|
||||
|
||||
// Exit if we failed to produce a valid patch for the existing layout.
|
||||
// This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for
|
||||
// example, if a panel is destroyed in the frontend but was never cleared in the backend, so the next time the backend
|
||||
// tries to update the layout, it attempts to insert only the changes against the old layout that no longer exists.
|
||||
if (diffObject === undefined) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("In `patchLayout`, the `diffObject` is undefined. The layout has not been updated. See the source code comment above this error for hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a list with a length, then set the length to 0 to clear the list
|
||||
if ("length" in diffObject) {
|
||||
diffObject.length = 0;
|
||||
}
|
||||
// Remove all of the keys from the old object
|
||||
Object.keys(diffObject).forEach((key) => delete (diffObject as Record<string, unknown>)[key]);
|
||||
|
||||
// Assign keys to the new object
|
||||
// `Object.assign` works but `diffObject = update.newValue;` doesn't.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
||||
Object.assign(diffObject, update.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
// Unpacking a layout group
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
||||
// Detect if this has already been parsed and, if so, return it as-is so this function can be idempotent
|
||||
if ("columnWidgets" in layoutGroup || "rowWidgets" in layoutGroup || "tableWidgets" in layoutGroup || ("name" in layoutGroup && "layout" in layoutGroup)) return layoutGroup;
|
||||
|
||||
if (layoutGroup.column) {
|
||||
const columnWidgets = layoutGroup.column.columnWidgets.map(parseWidgetInstance);
|
||||
|
||||
const result: WidgetSpanColumn = { columnWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutGroup.row) {
|
||||
const result: WidgetSpanRow = { rowWidgets: layoutGroup.row.rowWidgets.map(parseWidgetInstance) };
|
||||
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 = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tableWidgets: layoutGroup.table.tableWidgets.map((row: any) => row.map(parseWidgetInstance)),
|
||||
unstyled: layoutGroup.table.unstyled,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error("Layout row type does not exist");
|
||||
}
|
||||
Reference in New Issue
Block a user