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:
Keavon Chambers
2026-03-05 01:43:21 -08:00
committed by GitHub
parent f00a15a4c9
commit 8a1dfb9d8f
64 changed files with 1436 additions and 2093 deletions
@@ -3,9 +3,9 @@ name: Profiling Changes
on: on:
pull_request: pull_request:
paths: paths:
- 'node-graph/**' - "node-graph/**"
- 'Cargo.toml' - "Cargo.toml"
- 'Cargo.lock' - "Cargo.lock"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -200,7 +200,7 @@ jobs:
let commentBody = ""; let commentBody = "";
function formatNumber(num) { function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} }
function formatPercentage(pct) { function formatPercentage(pct) {
@@ -1,6 +1,7 @@
use std::path::PathBuf; #[cfg(target_os = "macos")]
use graphite_editor::messages::layout::utility_types::layout_widget::LayoutTarget;
use graphite_editor::messages::prelude::FrontendMessage; use graphite_editor::messages::prelude::FrontendMessage;
use std::path::PathBuf;
use super::DesktopWrapperMessageDispatcher; use super::DesktopWrapperMessageDispatcher;
use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
@@ -103,7 +104,10 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
FrontendMessage::UpdateMenuBarLayout { diff } => { FrontendMessage::UpdateLayout {
layout_target: LayoutTarget::MenuBar,
diff,
} => {
use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff}; use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff};
match diff.as_slice() { match diff.as_slice() {
[ [
+5 -1
View File
@@ -590,7 +590,11 @@ mod test {
for response in responses { for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file // Check for the existence of the file format incompatibility warning dialog after opening the test file
if let FrontendMessage::UpdateDialogColumn1 { diff } = response { if let FrontendMessage::UpdateLayout {
layout_target: LayoutTarget::DialogColumn1,
diff,
} = response
{
if let DiffUpdate::Layout(sub_layout) = &diff[0].new_value { if let DiffUpdate::Layout(sub_layout) = &diff[0].new_value {
if let LayoutGroup::Row { widgets } = &sub_layout.0[0] { if let LayoutGroup::Row { widgets } = &sub_layout.0[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &*widgets[0].widget { if let Widget::TextLabel(TextLabel { value, .. }) = &*widgets[0].widget {
@@ -39,7 +39,7 @@ pub enum FrontendMessage {
line_height_ratio: f64, line_height_ratio: f64,
#[serde(rename = "fontSize")] #[serde(rename = "fontSize")]
font_size: f64, font_size: f64,
color: Color, color: String,
#[serde(rename = "fontData")] #[serde(rename = "fontData")]
font_data: Vec<u8>, font_data: Vec<u8>,
transform: [f64; 6], transform: [f64; 6],
@@ -199,7 +199,9 @@ pub enum FrontendMessage {
UpdateLayersPanelState { UpdateLayersPanelState {
open: bool, open: bool,
}, },
UpdateDataPanelLayout { UpdateLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>, diff: Vec<WidgetDiff>,
}, },
UpdateImportReorderIndex { UpdateImportReorderIndex {
@@ -218,24 +220,12 @@ pub enum FrontendMessage {
#[serde(rename = "hasLeftInputWire")] #[serde(rename = "hasLeftInputWire")]
has_left_input_wire: HashMap<NodeId, bool>, has_left_input_wire: HashMap<NodeId, bool>,
}, },
UpdateDialogButtons {
diff: Vec<WidgetDiff>,
},
UpdateDialogColumn1 {
diff: Vec<WidgetDiff>,
},
UpdateDialogColumn2 {
diff: Vec<WidgetDiff>,
},
UpdateDocumentArtwork { UpdateDocumentArtwork {
svg: String, svg: String,
}, },
UpdateImageData { UpdateImageData {
image_data: Vec<(u64, Image<Color>)>, image_data: Vec<(u64, Image<Color>)>,
}, },
UpdateDocumentBarLayout {
diff: Vec<WidgetDiff>,
},
UpdateDocumentLayerDetails { UpdateDocumentLayerDetails {
data: LayerPanelEntry, data: LayerPanelEntry,
}, },
@@ -268,18 +258,6 @@ pub enum FrontendMessage {
UpdateGraphFadeArtwork { UpdateGraphFadeArtwork {
percentage: f64, percentage: f64,
}, },
UpdateLayersPanelControlBarLeftLayout {
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelControlBarRightLayout {
diff: Vec<WidgetDiff>,
},
UpdateLayersPanelBottomBarLayout {
diff: Vec<WidgetDiff>,
},
UpdateMenuBarLayout {
diff: Vec<WidgetDiff>,
},
UpdateMouseCursor { UpdateMouseCursor {
cursor: MouseCursorIcon, cursor: MouseCursorIcon,
}, },
@@ -296,9 +274,6 @@ pub enum FrontendMessage {
wires: Vec<WirePathUpdate>, wires: Vec<WirePathUpdate>,
}, },
ClearAllNodeGraphWires, ClearAllNodeGraphWires,
UpdateNodeGraphControlBarLayout {
diff: Vec<WidgetDiff>,
},
UpdateNodeGraphSelection { UpdateNodeGraphSelection {
selected: Vec<NodeId>, selected: Vec<NodeId>,
}, },
@@ -313,31 +288,10 @@ pub enum FrontendMessage {
#[serde(rename = "openDocuments")] #[serde(rename = "openDocuments")]
open_documents: Vec<OpenDocument>, open_documents: Vec<OpenDocument>,
}, },
UpdatePropertiesPanelLayout {
diff: Vec<WidgetDiff>,
},
UpdateToolOptionsLayout {
diff: Vec<WidgetDiff>,
},
UpdateToolShelfLayout {
diff: Vec<WidgetDiff>,
},
UpdateWirePathInProgress { UpdateWirePathInProgress {
#[serde(rename = "wirePath")] #[serde(rename = "wirePath")]
wire_path: Option<WirePath>, wire_path: Option<WirePath>,
}, },
UpdateWelcomeScreenButtonsLayout {
diff: Vec<WidgetDiff>,
},
UpdateStatusBarHintsLayout {
diff: Vec<WidgetDiff>,
},
UpdateStatusBarInfoLayout {
diff: Vec<WidgetDiff>,
},
UpdateWorkingColorsLayout {
diff: Vec<WidgetDiff>,
},
UpdatePlatform { UpdatePlatform {
platform: AppWindowPlatform, platform: AppWindowPlatform,
}, },
@@ -463,30 +463,11 @@ impl LayoutMessageHandler {
fn send_diff(&self, mut diff: Vec<WidgetDiff>, layout_target: LayoutTarget, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) { fn send_diff(&self, mut diff: Vec<WidgetDiff>, layout_target: LayoutTarget, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>) {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping)); diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target { if matches!(layout_target, LayoutTarget::_LayoutTargetLength) {
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { diff }, panic!("`_LayoutTargetLength` is not a valid `LayoutTarget` and is used for array indexing");
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { diff }, }
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { diff },
LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { diff },
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { diff },
LayoutTarget::StatusBarHints => FrontendMessage::UpdateStatusBarHintsLayout { diff },
LayoutTarget::StatusBarInfo => FrontendMessage::UpdateStatusBarInfoLayout { diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { diff },
LayoutTarget::WelcomeScreenButtons => FrontendMessage::UpdateWelcomeScreenButtonsLayout { diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { diff },
// KEEP THIS ENUM LAST responses.add(FrontendMessage::UpdateLayout { layout_target, diff });
LayoutTarget::_LayoutTargetLength => panic!("`_LayoutTargetLength` is not a valid `LayoutTarget` and is used for array indexing"),
};
responses.add(message);
} }
} }
@@ -847,7 +847,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
self.context_menu = Some(ContextMenuInformation { self.context_menu = Some(ContextMenuInformation {
context_menu_coordinates: (node_graph_point + node_graph_shift).into(), context_menu_coordinates: (node_graph_point + node_graph_shift).as_ivec2(),
context_menu_data, context_menu_data,
}); });
@@ -1280,7 +1280,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string(); let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string();
self.context_menu = Some(ContextMenuInformation { self.context_menu = Some(ContextMenuInformation {
context_menu_coordinates: (point + node_graph_shift).into(), context_menu_coordinates: (point + node_graph_shift).as_ivec2(),
context_menu_data: ContextMenuData::CreateNode { compatible_type }, context_menu_data: ContextMenuData::CreateNode { compatible_type },
}); });
@@ -2696,7 +2696,7 @@ impl NodeGraphMessageHandler {
position += IVec2::new(12, -12) position += IVec2::new(12, -12)
} }
Some(NodeGraphErrorDiagnostic { position: position.into(), error }) Some(NodeGraphErrorDiagnostic { position, error })
} }
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) { fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
@@ -2769,7 +2769,7 @@ impl NodeGraphMessageHandler {
children_allowed, children_allowed,
children_present: layer.has_children(network_interface.document_metadata()), children_present: layer.has_children(network_interface.document_metadata()),
expanded: layer.has_children(network_interface.document_metadata()) && !collapsed.0.contains(&layer), expanded: layer.has_children(network_interface.document_metadata()) && !collapsed.0.contains(&layer),
depth: layer.ancestors(network_interface.document_metadata()).count() - 1, depth: layer.ancestors(network_interface.document_metadata()).count() as u32 - 1,
visible: network_interface.is_visible(&node_id, &[]), visible: network_interface.is_visible(&node_id, &[]),
parents_visible, parents_visible,
unlocked: !network_interface.is_locked(&node_id, &[]), unlocked: !network_interface.is_locked(&node_id, &[]),
@@ -1,4 +1,4 @@
use glam::{DVec2, IVec2}; use glam::IVec2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::Type; use graphene_std::Type;
@@ -162,14 +162,14 @@ pub enum ContextMenuData {
pub struct ContextMenuInformation { pub struct ContextMenuInformation {
// Stores whether the context menu is open and its position in graph coordinates // Stores whether the context menu is open and its position in graph coordinates
#[serde(rename = "contextMenuCoordinates")] #[serde(rename = "contextMenuCoordinates")]
pub context_menu_coordinates: FrontendXY, pub context_menu_coordinates: IVec2,
#[serde(rename = "contextMenuData")] #[serde(rename = "contextMenuData")]
pub context_menu_data: ContextMenuData, pub context_menu_data: ContextMenuData,
} }
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct NodeGraphErrorDiagnostic { pub struct NodeGraphErrorDiagnostic {
pub position: FrontendXY, pub position: IVec2,
pub error: String, pub error: String,
} }
@@ -196,22 +196,3 @@ pub enum Direction {
Left, Left,
Right, Right,
} }
/// Stores node graph coordinates which are then transformed in Svelte based on the node graph transform
#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct FrontendXY {
pub x: i32,
pub y: i32,
}
impl From<DVec2> for FrontendXY {
fn from(v: DVec2) -> Self {
FrontendXY { x: v.x as i32, y: v.y as i32 }
}
}
impl From<IVec2> for FrontendXY {
fn from(v: IVec2) -> Self {
FrontendXY { x: v.x, y: v.y }
}
}
@@ -28,7 +28,7 @@ pub struct LayerPanelEntry {
#[serde(rename = "childrenPresent")] #[serde(rename = "childrenPresent")]
pub children_present: bool, pub children_present: bool,
pub expanded: bool, pub expanded: bool,
pub depth: usize, pub depth: u32,
pub visible: bool, pub visible: bool,
#[serde(rename = "parentsVisible")] #[serde(rename = "parentsVisible")]
pub parents_visible: bool, pub parents_visible: bool,
@@ -412,7 +412,7 @@ impl TextToolData {
text: editing_text.text.clone(), text: editing_text.text.clone(),
line_height_ratio: editing_text.typesetting.line_height_ratio, line_height_ratio: editing_text.typesetting.line_height_ratio,
font_size: editing_text.typesetting.font_size, font_size: editing_text.typesetting.font_size,
color: editing_text.color.unwrap_or(Color::BLACK), color: editing_text.color.map_or("#000000".to_string(), |color| format!("#{}", color.to_rgba_hex_srgb())),
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default(), font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default(),
transform: editing_text.transform.to_cols_array(), transform: editing_text.transform.to_cols_array(),
max_width: editing_text.typesetting.max_width, max_width: editing_text.typesetting.max_width,
+1
View File
@@ -87,6 +87,7 @@ export default defineConfig([
"svelte/require-each-key": "off", // TODO: Remove this rule and fix the places where it's violated "svelte/require-each-key": "off", // TODO: Remove this rule and fix the places where it's violated
// Import plugin config (for intelligently validating module import statements) // Import plugin config (for intelligently validating module import statements)
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"import/no-unresolved": "error", "import/no-unresolved": "error",
// `no-duplicates` disabled due to <https://github.com/import-js/eslint-plugin-import/issues/1479#issuecomment-1789527447>. Reenable if that issue gets fixed. // `no-duplicates` disabled due to <https://github.com/import-js/eslint-plugin-import/issues/1479#issuecomment-1789527447>. Reenable if that issue gets fixed.
"import/no-duplicates": "off", "import/no-duplicates": "off",
+116 -106
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -32,9 +32,7 @@
}, },
"//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders a pixel above its intended position.", "//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders a pixel above its intended position.",
"dependencies": { "dependencies": {
"class-transformer": "^0.5.1",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"reflect-metadata": "^0.2.2",
"source-code-pro": "2.42.0", "source-code-pro": "2.42.0",
"source-sans-pro": "2.45.0" "source-sans-pro": "2.45.0"
}, },
@@ -53,18 +51,18 @@
"globals": "^17.0.0", "globals": "^17.0.0",
"license-checker-rseidelsohn": "^4.4.2", "license-checker-rseidelsohn": "^4.4.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier-plugin-svelte": "^3.4.1",
"prettier": "^3.8.0", "prettier": "^3.8.0",
"prettier-plugin-svelte": "^3.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"sass": "^1.97.2", "sass": "^1.97.2",
"svelte-preprocess": "^6.0.3",
"svelte": "5.47.1", "svelte": "5.47.1",
"svelte-preprocess": "^6.0.3",
"tar": "^7.5.4", "tar": "^7.5.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript-eslint": "^8.53.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-multiple-assets": "2.2.6", "typescript-eslint": "^8.53.1",
"vite": "^7.3.1" "vite": "^7.3.1",
"vite-multiple-assets": "2.2.6"
}, },
"homepage": "https://graphite.art", "homepage": "https://graphite.art",
"license": "Apache-2.0", "license": "Apache-2.0",
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; 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"; import Editor from "@graphite/components/Editor.svelte";
+1 -1
View File
@@ -38,7 +38,7 @@ Defines the message formats and data types received from the backend. Since Rust
## Subscription router: `subscription-router.ts` ## 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` ## Svelte app entry point: `App.svelte`
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, setContext } from "svelte"; 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 { createClipboardManager } from "@graphite/io-managers/clipboard";
import { createHyperlinkManager } from "@graphite/io-managers/hyperlink"; import { createHyperlinkManager } from "@graphite/io-managers/hyperlink";
import { createInputManager } from "@graphite/io-managers/input"; import { createInputManager } from "@graphite/io-managers/input";
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte"; 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 { import {
type Color,
contrastingOutlineFactor, contrastingOutlineFactor,
isColor, isColor,
isGradient, isGradient,
@@ -12,7 +13,7 @@
createColorFromHSVA, createColorFromHSVA,
colorFromCSS, colorFromCSS,
colorToRgb255, colorToRgb255,
colorToHSVA, colorToHSV,
colorToHexOptionalAlpha, colorToHexOptionalAlpha,
colorToHexNoAlpha, colorToHexNoAlpha,
colorToRgbCSS, colorToRgbCSS,
@@ -20,8 +21,8 @@
colorOpaque, colorOpaque,
colorEquals, colorEquals,
gradientFirstColor, gradientFirstColor,
} from "@graphite/messages"; } from "@graphite/utility-functions/colors";
import type { TooltipState } from "@graphite/state-providers/tooltip"; import type { HSV, RGB } from "@graphite/utility-functions/colors";
import { clamp } from "@graphite/utility-functions/math"; import { clamp } from "@graphite/utility-functions/math";
import { isDesktop } from "@graphite/utility-functions/platform"; import { isDesktop } from "@graphite/utility-functions/platform";
@@ -70,8 +71,8 @@
export let open: boolean; export let open: boolean;
const colorForHSVA = isColor(colorOrGradient) ? colorOrGradient : gradientFirstColor(colorOrGradient); const colorForHSVA = isColor(colorOrGradient) ? colorOrGradient : gradientFirstColor(colorOrGradient);
const hsvaOrNone = colorForHSVA ? colorToHSVA(colorForHSVA) : undefined; const hsvOrNone = colorForHSVA ? colorToHSV(colorForHSVA) : undefined;
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 }; const hsv = hsvOrNone || { h: 0, s: 0, v: 0 };
// Gradient color stops // Gradient color stops
$: gradient = isGradient(colorOrGradient) ? colorOrGradient : undefined; $: gradient = isGradient(colorOrGradient) ? colorOrGradient : undefined;
@@ -81,17 +82,17 @@
// Currently viewed color // Currently viewed color
$: color = isColor(colorOrGradient) ? colorOrGradient : selectedGradientColor; $: color = isColor(colorOrGradient) ? colorOrGradient : selectedGradientColor;
// New color components // New color components
let hue = hsva.h; let hue = hsv.h;
let saturation = hsva.s; let saturation = hsv.s;
let value = hsva.v; let value = hsv.v;
let alpha = hsva.a; let alpha = colorForHSVA ? colorForHSVA.alpha : 1;
let isNone = hsvaOrNone === undefined; let isNone = hsvOrNone === undefined;
// Old color components // Old color components
let oldHue = hsva.h; let oldHue = hsv.h;
let oldSaturation = hsva.s; let oldSaturation = hsv.s;
let oldValue = hsva.v; let oldValue = hsv.v;
let oldAlpha = hsva.a; let oldAlpha = colorForHSVA ? colorForHSVA.alpha : 1;
let oldIsNone = hsvaOrNone === undefined; let oldIsNone = hsvOrNone === undefined;
// Transient state // Transient state
let draggingPickerTrack: HTMLDivElement | undefined = undefined; let draggingPickerTrack: HTMLDivElement | undefined = undefined;
let strayCloses = true; let strayCloses = true;
@@ -114,8 +115,8 @@
$: watchOpen(open); $: watchOpen(open);
$: watchColor(color); $: watchColor(color);
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone); $: oldColor = oldIsNone ? createNoneColor() : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha);
$: newColor = generateColor(hue, saturation, value, alpha, isNone); $: newColor = isNone ? createNoneColor() : createColorFromHSVA(hue, saturation, value, alpha);
$: rgbChannels = Object.entries(colorToRgb255(newColor) || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; $: 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][]; $: 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); $: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1);
@@ -123,11 +124,6 @@
$: outlined = outlineFactor > 0.0001; $: outlined = outlineFactor > 0.0001;
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1; $: 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) { async function watchOpen(open: boolean) {
if (open) { if (open) {
setTimeout(() => hexCodeInputWidget?.focus(), 0); setTimeout(() => hexCodeInputWidget?.focus(), 0);
@@ -138,9 +134,9 @@
} }
function watchColor(color: Color) { function watchColor(color: Color) {
const hsva = colorToHSVA(color); const hsv = colorToHSV(color);
if (hsva === undefined) { if (hsv === undefined) {
setNewHSVA(0, 0, 0, 1, true); setNewHSVA(0, 0, 0, 1, true);
return; return;
} }
@@ -149,14 +145,14 @@
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom) // - ...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 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 // - ...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: // 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 // - ...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 // Update the value
value = hsva.v; value = hsv.v;
// Update the alpha // Update the alpha
alpha = hsva.a; alpha = color.alpha;
// Update the status of this not being a color // Update the status of this not being a color
isNone = false; isNone = false;
} }
@@ -375,9 +371,10 @@
setColor(createNoneColor()); setColor(createNoneColor());
} else { } else {
const presetColor = createColor(...PURE_COLORS[preset], 1); 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); setColor(presetColor);
} }
} }
@@ -425,13 +422,13 @@
activeIndexIsMidpoint = activeMarkerIsMidpoint; activeIndexIsMidpoint = activeMarkerIsMidpoint;
const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex]; const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex];
const hsva = color ? colorToHSVA(color) : undefined; const hsv = color ? colorToHSV(color) : undefined;
if (!color || !hsva) return; if (!color || !hsv) return;
setColor(color); setColor(color);
setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none); setNewHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none);
setOldHSVA(hsva.h, hsva.s, hsva.v, hsva.a, color.none); setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none);
} }
export function div(): HTMLDivElement | undefined { export function div(): HTMLDivElement | undefined {
+4 -3
View File
@@ -2,7 +2,8 @@
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor"; 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 LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
@@ -12,14 +13,14 @@
let dataPanelLayout: Layout = []; let dataPanelLayout: Layout = [];
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateDataPanelLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
patchLayout(dataPanelLayout, data); patchLayout(dataPanelLayout, data);
dataPanelLayout = dataPanelLayout; dataPanelLayout = dataPanelLayout;
}); });
}); });
onDestroy(() => { onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateDataPanelLayout); editor.subscriptions.unsubscribeLayoutUpdate("DataPanel");
}); });
</script> </script>
+53 -54
View File
@@ -2,33 +2,15 @@
import { getContext, onMount, onDestroy, tick } from "svelte"; import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { import type { Color, FrontendMessages, MenuDirection } from "@graphite/messages";
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 { AppWindowState } from "@graphite/state-providers/app-window"; import type { AppWindowState } from "@graphite/state-providers/app-window";
import type { DocumentState } from "@graphite/state-providers/document"; import type { DocumentState } from "@graphite/state-providers/document";
import { isColor, createColor } from "@graphite/utility-functions/colors";
import { pasteFile } from "@graphite/utility-functions/files"; import { pasteFile } from "@graphite/utility-functions/files";
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports"; 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 ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.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 ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
type DisplayEditableTextbox = FrontendMessages["DisplayEditableTextbox"];
let rulerHorizontal: RulerInput | undefined; let rulerHorizontal: RulerInput | undefined;
let rulerVertical: RulerInput | undefined; let rulerVertical: RulerInput | undefined;
let viewport: HTMLDivElement | undefined; let viewport: HTMLDivElement | undefined;
@@ -54,12 +38,12 @@
let textInputMatrix: number[]; let textInputMatrix: number[];
// Scrollbars // Scrollbars
let scrollbarPos: XY = { x: 0.5, y: 0.5 }; let scrollbarPos = { x: 0.5, y: 0.5 };
let scrollbarSize: XY = { x: 0.5, y: 0.5 }; let scrollbarSize = { x: 0.5, y: 0.5 };
let scrollbarMultiplier: XY = { x: 0, y: 0 }; let scrollbarMultiplier = { x: 0, y: 0 };
// Rulers // Rulers
let rulerOrigin: XY = { x: 0, y: 0 }; let rulerOrigin = { x: 0, y: 0 };
let rulerSpacing = 100; let rulerSpacing = 100;
let rulerInterval = 100; let rulerInterval = 100;
let rulersVisible = true; let rulersVisible = true;
@@ -227,7 +211,7 @@
export async function updateEyedropperSamplingState( export async function updateEyedropperSamplingState(
// `image` is currently only used for Vello renders // `image` is currently only used for Vello renders
image: ImageData | undefined, image: ImageData | undefined,
mousePosition: XY | undefined, mousePosition: [number, number] | undefined,
colorPrimary: string, colorPrimary: string,
colorSecondary: string, colorSecondary: string,
): Promise<[number, number, number] | undefined> { ): Promise<[number, number, number] | undefined> {
@@ -239,8 +223,8 @@
if (canvasWidth === undefined || canvasHeight === undefined) return undefined; if (canvasWidth === undefined || canvasHeight === undefined) return undefined;
cursorLeft = mousePosition.x; cursorLeft = mousePosition[0];
cursorTop = mousePosition.y; cursorTop = mousePosition[1];
let preview = image; let preview = image;
if (!preview) { if (!preview) {
@@ -262,8 +246,8 @@
if (!rasterizedContext) return undefined; if (!rasterizedContext) return undefined;
preview = rasterizedContext.getImageData( preview = rasterizedContext.getImageData(
mousePosition.x * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, mousePosition[0] * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
mousePosition.y * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2, mousePosition[1] * dpiFactor - (ZOOM_WINDOW_DIMENSIONS - 1) / 2,
ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS,
ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS,
); );
@@ -293,25 +277,41 @@
} }
// Update scrollbars and rulers // Update scrollbars and rulers
export function updateDocumentScrollbars(position: XY, size: XY, multiplier: XY) { export function updateDocumentScrollbars(position: [number, number], size: [number, number], multiplier: [number, number]) {
scrollbarPos = position; scrollbarPos = { x: position[0], y: position[1] };
scrollbarSize = size; scrollbarSize = { x: size[0], y: size[1] };
scrollbarMultiplier = multiplier; scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
} }
export function updateDocumentRulers(origin: XY, spacing: number, interval: number, visible: boolean) { export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean) {
rulerOrigin = origin; rulerOrigin = { x: origin[0], y: origin[1] };
rulerSpacing = spacing; rulerSpacing = spacing;
rulerInterval = interval; rulerInterval = interval;
rulersVisible = visible; rulersVisible = visible;
} }
// Update mouse cursor icon // Update mouse cursor icon
export function updateMouseCursor(cursor: MouseCursorIcon) { export function updateMouseCursor(cursor: string) {
let cursorString: string = cursor; 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) // 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 = ` const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20"> <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" /> <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.height = height;
textInput.style.lineHeight = `${data.lineHeightRatio}`; textInput.style.lineHeight = `${data.lineHeightRatio}`;
textInput.style.fontSize = `${data.fontSize}px`; textInput.style.fontSize = `${data.fontSize}px`;
textInput.style.color = colorToHexOptionalAlpha(data.color) || "transparent"; textInput.style.color = data.color;
textInput.style.textAlign = data.align; textInput.style.textAlign = data.align;
textInput.oninput = () => { textInput.oninput = () => {
@@ -418,7 +418,7 @@
// which provides pixel-perfect physical dimensions via devicePixelContentBoxSize // 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; const picker = (gradientStopPicker?.div()?.querySelector("[data-floating-menu-content]") || undefined) as HTMLElement | undefined;
if (!picker || !position || !viewport) return "Bottom"; if (!picker || !position || !viewport) return "Bottom";
@@ -448,12 +448,12 @@
updatePixelRatio(); updatePixelRatio();
// Update rendered SVGs // Update rendered SVGs
editor.subscriptions.subscribeJsMessage(UpdateDocumentArtwork, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateDocumentArtwork", async (data) => {
await tick(); await tick();
updateDocumentArtwork(data.svg); updateDocumentArtwork(data.svg);
}); });
editor.subscriptions.subscribeJsMessage(UpdateEyedropperSamplingState, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateEyedropperSamplingState", async (data) => {
await tick(); await tick();
const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data; const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data;
@@ -467,19 +467,19 @@
}); });
// Gradient stop color picker // Gradient stop color picker
editor.subscriptions.subscribeJsMessage(UpdateGradientStopColorPickerPosition, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateGradientStopColorPickerPosition", (data) => {
gradientStopPickerColor = data.color; gradientStopPickerColor = data.color;
gradientStopPickerPosition = { x: data.x, y: data.y }; gradientStopPickerPosition = { x: data.x, y: data.y };
}); });
// Update scrollbars and rulers // Update scrollbars and rulers
editor.subscriptions.subscribeJsMessage(UpdateDocumentScrollbars, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateDocumentScrollbars", async (data) => {
await tick(); await tick();
const { position, size, multiplier } = data; const { position, size, multiplier } = data;
updateDocumentScrollbars(position, size, multiplier); updateDocumentScrollbars(position, size, multiplier);
}); });
editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
await tick(); await tick();
const { origin, spacing, interval, visible } = data; const { origin, spacing, interval, visible } = data;
@@ -487,25 +487,24 @@
}); });
// Update mouse cursor icon // Update mouse cursor icon
editor.subscriptions.subscribeJsMessage(UpdateMouseCursor, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateMouseCursor", async (data) => {
await tick(); await tick();
const { cursor } = data; updateMouseCursor(data.cursor);
updateMouseCursor(cursor);
}); });
// Text entry // Text entry
editor.subscriptions.subscribeJsMessage(TriggerTextCommit, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerTextCommit", async () => {
await tick(); await tick();
triggerTextCommit(); triggerTextCommit();
}); });
editor.subscriptions.subscribeJsMessage(DisplayEditableTextbox, async (data) => { editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextbox", async (data) => {
await tick(); await tick();
displayEditableTextbox(data); displayEditableTextbox(data);
}); });
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxUpdateFontData, async (data) => { editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxUpdateFontData", async (data) => {
await tick(); await tick();
const fontData = new Uint8Array(data.fontData); const fontData = new Uint8Array(data.fontData);
@@ -514,10 +513,10 @@
textInput.style.fontFamily = "text-font"; textInput.style.fontFamily = "text-font";
} }
}); });
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => { editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxTransform", async (data) => {
textInputMatrix = data.transform; textInputMatrix = data.transform;
}); });
editor.subscriptions.subscribeJsMessage(DisplayRemoveEditableTextbox, async () => { editor.subscriptions.subscribeFrontendMessage("DisplayRemoveEditableTextbox", async () => {
await tick(); await tick();
displayRemoveEditableTextbox(); displayRemoveEditableTextbox();
+11 -18
View File
@@ -3,19 +3,12 @@
import { SvelteMap } from "svelte/reactivity"; import { SvelteMap } from "svelte/reactivity";
import type { Editor } from "@graphite/editor"; 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 { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { TooltipState } from "@graphite/state-providers/tooltip"; import type { TooltipState } from "@graphite/state-providers/tooltip";
import { pasteFile } from "@graphite/utility-functions/files"; import { pasteFile } from "@graphite/utility-functions/files";
import { operatingSystem } from "@graphite/utility-functions/platform"; import { operatingSystem } from "@graphite/utility-functions/platform";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -76,26 +69,26 @@
let layersPanelBottomBarLayout: Layout = []; let layersPanelBottomBarLayout: Layout = [];
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlLeftBar", (data) => {
patchLayout(layersPanelControlBarLeftLayout, data); patchLayout(layersPanelControlBarLeftLayout, data);
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout; layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
}); });
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarRightLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlRightBar", (data) => {
patchLayout(layersPanelControlBarRightLayout, data); patchLayout(layersPanelControlBarRightLayout, data);
layersPanelControlBarRightLayout = layersPanelControlBarRightLayout; layersPanelControlBarRightLayout = layersPanelControlBarRightLayout;
}); });
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelBottomBarLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("LayersPanelBottomBar", (data) => {
patchLayout(layersPanelBottomBarLayout, data); patchLayout(layersPanelBottomBarLayout, data);
layersPanelBottomBarLayout = layersPanelBottomBarLayout; layersPanelBottomBarLayout = layersPanelBottomBarLayout;
}); });
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructure, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerStructure", (data) => {
rebuildLayerHierarchy(data.layerStructure); rebuildLayerHierarchy(data.layerStructure);
}); });
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerDetails", (data) => {
const targetLayer = data.data; const targetLayer = data.data;
const targetId = targetLayer.id; const targetId = targetLayer.id;
@@ -114,11 +107,11 @@
}); });
onDestroy(() => { onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout); editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlLeftBar");
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout); editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlRightBar");
editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout); editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelBottomBar");
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructure); editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerStructure");
editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails); editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerDetails");
removeEventListener("pointerup", draggingPointerUp); removeEventListener("pointerup", draggingPointerUp);
removeEventListener("pointermove", draggingPointerMove); removeEventListener("pointermove", draggingPointerMove);
@@ -2,7 +2,8 @@
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onMount, onDestroy } from "svelte";
import type { Editor } from "@graphite/editor"; 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 LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
@@ -12,14 +13,14 @@
let propertiesPanelLayout: Layout = []; let propertiesPanelLayout: Layout = [];
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("PropertiesPanel", (data) => {
patchLayout(propertiesPanelLayout, data); patchLayout(propertiesPanelLayout, data);
propertiesPanelLayout = propertiesPanelLayout; propertiesPanelLayout = propertiesPanelLayout;
}); });
}); });
onDestroy(() => { onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdatePropertiesPanelLayout); editor.subscriptions.unsubscribeLayoutUpdate("PropertiesPanel");
}); });
</script> </script>
@@ -3,9 +3,9 @@
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages"; import type { Layout } from "@graphite/messages";
import { patchLayout, UpdateWelcomeScreenButtonsLayout } from "@graphite/messages";
import { pasteFile } from "@graphite/utility-functions/files"; import { pasteFile } from "@graphite/utility-functions/files";
import { isDesktop } from "@graphite/utility-functions/platform"; import { isDesktop } from "@graphite/utility-functions/platform";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -18,14 +18,14 @@
let welcomePanelButtonsLayout: Layout = []; let welcomePanelButtonsLayout: Layout = [];
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateWelcomeScreenButtonsLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("WelcomeScreenButtons", (data) => {
patchLayout(welcomePanelButtonsLayout, data); patchLayout(welcomePanelButtonsLayout, data);
welcomePanelButtonsLayout = welcomePanelButtonsLayout; welcomePanelButtonsLayout = welcomePanelButtonsLayout;
}); });
}); });
onDestroy(() => { onDestroy(() => {
editor.subscriptions.unsubscribeJsMessage(UpdateWelcomeScreenButtonsLayout); editor.subscriptions.unsubscribeLayoutUpdate("WelcomeScreenButtons");
}); });
function dropFile(e: DragEvent) { function dropFile(e: DragEvent) {
+31 -31
View File
@@ -108,7 +108,7 @@
function createNode(identifier: string) { function createNode(identifier: string) {
if ($nodeGraph.contextMenuInformation === undefined) return; 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 { function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string {
@@ -203,8 +203,8 @@
class="context-menu" class="context-menu"
data-context-menu data-context-menu
styles={{ styles={{
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`, left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[0] * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`, top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[1] * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
}} }}
open={true} open={true}
type="Popover" type="Popover"
@@ -257,10 +257,10 @@
{#if $nodeGraph.error} {#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})`}> <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} {$nodeGraph.error.error}
</span> </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} {$nodeGraph.error.error}
</span> </span>
</div> </div>
@@ -324,8 +324,8 @@
data-datatype={frontendOutput.dataType} data-datatype={frontendOutput.dataType}
style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 8) / 24 + index}
> >
{#if frontendOutput.connectedTo.length > 0} {#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)" /> <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="edit-import-export import"
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 8) / 24 + index}
> >
{#if editingNameImportIndex === index} {#if editingNameImportIndex === index}
<input <input
@@ -377,8 +377,8 @@
{:else} {:else}
<div <div
class="plus" class="plus"
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
> >
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} /> <IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} />
</div> </div>
@@ -397,8 +397,8 @@
data-datatype={frontendInput.dataType} data-datatype={frontendInput.dataType}
style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`} style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 8) / 24 + index}
> >
{#if frontendInput.connectedTo !== "Connected to nothing."} {#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)" /> <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="edit-import-export export"
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 8) / 24 + index}
> >
{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{#if index > 0} {#if index > 0}
@@ -448,8 +448,8 @@
{:else} {:else}
<div <div
class="plus" class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24}
> >
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} /> <IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} />
</div> </div>
@@ -459,15 +459,15 @@
{#if $nodeGraph.updateImportsExports.addImportExport} {#if $nodeGraph.updateImportsExports.addImportExport}
<div <div
class="plus" class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.imports.length} style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
> >
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} /> <IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
</div> </div>
<div <div
class="plus" class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.exports.length} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
> >
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryExport()} /> <IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryExport()} />
</div> </div>
@@ -475,16 +475,16 @@
{#if $nodeGraph.reorderImportIndex !== undefined} {#if $nodeGraph.reorderImportIndex !== undefined}
{@const position = { {@const position = {
x: Number($nodeGraph.updateImportsExports.importPosition.x), x: Number($nodeGraph.updateImportsExports.importPosition[0]),
y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, 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> <div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24}></div>
{/if} {/if}
{#if $nodeGraph.reorderExportIndex !== undefined} {#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = { {@const position = {
x: Number($nodeGraph.updateImportsExports.exportPosition.x), x: Number($nodeGraph.updateImportsExports.exportPosition[0]),
y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, 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> <div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24}></div>
{/if} {/if}
@@ -510,8 +510,8 @@
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={!node.visible} class:disabled={!node.visible}
class:locked={node.locked} class:locked={node.locked}
style:--offset-left={node.position?.x || 0} style:--offset-left={node.position?.[0] || 0}
style:--offset-top={node.position?.y || 0} style:--offset-top={node.position?.[1] || 0}
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} 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} style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
data-tooltip-label={nodeNameTooltipLabel(node)} data-tooltip-label={nodeNameTooltipLabel(node)}
data-tooltip-description={` 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()} `.trim()}
data-node={node.id} data-node={node.id}
> >
@@ -675,14 +675,14 @@
class:selected={$nodeGraph.selected.includes(node.id)} class:selected={$nodeGraph.selected.includes(node.id)}
class:previewed={node.previewed} class:previewed={node.previewed}
class:disabled={!node.visible} class:disabled={!node.visible}
style:--offset-left={node.position?.x || 0} style:--offset-left={node.position?.[0] || 0}
style:--offset-top={node.position?.y || 0} style:--offset-top={node.position?.[1] || 0}
style:--clip-path-id={`url(#${clipPathId})`} style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`} style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`} style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
data-tooltip-label={nodeNameTooltipLabel(node)} data-tooltip-label={nodeNameTooltipLabel(node)}
data-tooltip-description={` 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()} `.trim()}
data-node={node.id} data-node={node.id}
> >
@@ -1,5 +1,6 @@
<script lang="ts"> <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 WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte"; import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
@@ -2,14 +2,15 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Editor } from "@graphite/editor"; 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 LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte"; import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let widgetData: WidgetSectionFromJsMessages; export let widgetData: WidgetSectionData;
export let layoutTarget: LayoutTarget; export let layoutTarget: LayoutTarget;
let className = ""; let className = "";
@@ -3,8 +3,9 @@
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages"; 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 { 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 NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte"; import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
@@ -97,6 +98,7 @@
component: ColorInput, component: ColorInput,
getProps: (props: WidgetTypes["ColorInput"], index) => ({ getProps: (props: WidgetTypes["ColorInput"], index) => ({
...exclude(props), ...exclude(props),
value: parseFillChoice(props.value),
$$events: { $$events: {
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
startHistoryTransaction: () => widgetValueCommit(index, props.value), startHistoryTransaction: () => widgetValueCommit(index, props.value),
@@ -191,6 +193,7 @@
getProps: (props: WidgetTypes["PopoverButton"]) => ({ getProps: (props: WidgetTypes["PopoverButton"]) => ({
...exclude(props), ...exclude(props),
layoutTarget, layoutTarget,
popoverLayout: props.popoverLayout.map(createLayoutGroup),
}), }),
}, },
RadioInput: { RadioInput: {
@@ -1,9 +1,9 @@
<script lang="ts"> <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"; import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
export let widgetData: WidgetTableFromJsMessages; export let widgetData: WidgetTableData;
export let layoutTarget: LayoutTarget; export let layoutTarget: LayoutTarget;
export let unstyled = false; export let unstyled = false;
@@ -1,5 +1,5 @@
<script lang="ts"> <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 type { ActionShortcut } from "@graphite/messages";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -1,5 +1,5 @@
<script lang="ts"> <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"; import type { MenuDirection, ActionShortcut, Layout, LayoutTarget } from "@graphite/messages";
@@ -2,7 +2,8 @@
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages"; 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 ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; 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 LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -7,7 +7,8 @@
import { createEventDispatcher, onDestroy } from "svelte"; import { createEventDispatcher, onDestroy } from "svelte";
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm"; 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 { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -2,7 +2,8 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Editor } from "@graphite/editor"; 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 ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -1,5 +1,6 @@
<script lang="ts"> <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 type { ActionShortcut } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { type SeparatorDirection, type SeparatorStyle } from "@graphite/messages"; import type { SeparatorDirection, SeparatorStyle } from "@graphite/messages";
// Content // Content
export let direction: SeparatorDirection = "Horizontal"; export let direction: SeparatorDirection = "Horizontal";
@@ -3,7 +3,7 @@
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages"; 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 LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import Separator from "@graphite/components/widgets/labels/Separator.svelte";
@@ -15,11 +15,11 @@
let statusBarInfoLayout: Layout = []; let statusBarInfoLayout: Layout = [];
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateStatusBarHintsLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("StatusBarHints", (data) => {
patchLayout(statusBarHintsLayout, data); patchLayout(statusBarHintsLayout, data);
statusBarHintsLayout = statusBarHintsLayout; statusBarHintsLayout = statusBarHintsLayout;
}); });
editor.subscriptions.subscribeJsMessage(UpdateStatusBarInfoLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("StatusBarInfo", (data) => {
patchLayout(statusBarInfoLayout, data); patchLayout(statusBarInfoLayout, data);
statusBarInfoLayout = statusBarInfoLayout; statusBarInfoLayout = statusBarInfoLayout;
}); });
@@ -3,10 +3,10 @@
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages"; import type { Layout } from "@graphite/messages";
import { patchLayout, UpdateMenuBarLayout } from "@graphite/messages";
import type { AppWindowState } from "@graphite/state-providers/app-window"; import type { AppWindowState } from "@graphite/state-providers/app-window";
import type { FullscreenState } from "@graphite/state-providers/fullscreen"; import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import type { TooltipState } from "@graphite/state-providers/tooltip"; import type { TooltipState } from "@graphite/state-providers/tooltip";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
@@ -26,7 +26,7 @@
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28; $: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
onMount(() => { onMount(() => {
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("MenuBar", (data) => {
patchLayout(menuBarLayout, data); patchLayout(menuBarLayout, data);
menuBarLayout = menuBarLayout; menuBarLayout = menuBarLayout;
}); });
+5 -5
View File
@@ -2,8 +2,9 @@
import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm"; import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm"; import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import { type JsMessageType } from "@graphite/messages"; import type { FrontendMessages } from "@graphite/messages";
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router"; import { createSubscriptionRouter } from "@graphite/subscription-router";
import type { SubscriptionRouter } from "@graphite/subscription-router";
import { operatingSystem } from "@graphite/utility-functions/platform"; 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`. // 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); const randomSeed = BigInt(randomSeedFloat);
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen) // 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` // 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.handleFrontendMessage(messageType, messageData);
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
}); });
// Subscriptions: allows subscribing to messages in JS that are sent from the Wasm backend // Subscriptions: allows subscribing to messages in JS that are sent from the Wasm backend
+5 -6
View File
@@ -1,16 +1,15 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { TriggerClipboardWrite, TriggerSelectionRead, TriggerSelectionWrite } from "@graphite/messages";
export function createClipboardManager(editor: Editor) { export function createClipboardManager(editor: Editor) {
// Subscribe to process backend event // 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 // If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(data.content); 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.handle.readSelection(readAtCaret(data.cut), data.cut);
}); });
editor.subscriptions.subscribeJsMessage(TriggerSelectionWrite, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
insertAtCaret(data.content); insertAtCaret(data.content);
}); });
} }
@@ -44,7 +43,7 @@ function readAtCaret(cut: boolean): string | undefined {
return undefined; return undefined;
} }
const selectedText = selection.toString(); const selectedText = String(selection);
if (!selectedText) return undefined; if (!selectedText) return undefined;
if (cut) { if (cut) {
+3 -4
View File
@@ -1,5 +1,4 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { TriggerFontCatalogLoad, TriggerFontDataLoad } from "@graphite/messages";
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[]; 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) { export function createFontsManager(editor: Editor) {
// Subscribe to process backend events // Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(TriggerFontCatalogLoad, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
const response = await fetch(FONT_LIST_API); const response = await fetch(FONT_LIST_API);
const fontListResponse = (await response.json()) as { items: ApiResponse }; const fontListResponse = (await response.json()) as { items: ApiResponse };
const fontListData = fontListResponse.items; const fontListData = fontListResponse.items;
@@ -26,7 +25,7 @@ export function createFontsManager(editor: Editor) {
editor.handle.onFontCatalogLoad(catalog); editor.handle.onFontCatalogLoad(catalog);
}); });
editor.subscriptions.subscribeJsMessage(TriggerFontDataLoad, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
const { fontFamily, fontStyle } = data.font; const { fontFamily, fontStyle } = data.font;
try { try {
+2 -3
View File
@@ -1,9 +1,8 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { TriggerVisitLink } from "@graphite/messages";
export function createHyperlinkManager(editor: Editor) { export function createHyperlinkManager(editor: Editor) {
// Subscribe to process backend event // Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerVisitLink, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
window.open(data.url, "_blank"); window.open(data.url, "_blank");
}); });
} }
+7 -8
View File
@@ -1,11 +1,10 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { TriggerClipboardRead, WindowPointerLockMove } from "@graphite/messages"; import type { DialogState } from "@graphite/state-providers/dialog";
import { type DialogState } from "@graphite/state-providers/dialog"; import type { DocumentState } from "@graphite/state-providers/document";
import { type DocumentState } from "@graphite/state-providers/document"; import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import { type FullscreenState } from "@graphite/state-providers/fullscreen"; import type { PortfolioState } from "@graphite/state-providers/portfolio";
import { type PortfolioState } from "@graphite/state-providers/portfolio";
import { pasteFile } from "@graphite/utility-functions/files"; import { pasteFile } from "@graphite/utility-functions/files";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry"; import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform"; import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform";
@@ -392,7 +391,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// Frontend message subscriptions // 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 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 // In the catch block, explain to the user why the paste failed and how to fix or work around the problem
try { try {
@@ -487,7 +486,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}); });
// Pointer lock movement events on desktop // Pointer lock movement events on desktop
editor.subscriptions.subscribeJsMessage(WindowPointerLockMove, (data) => { editor.subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => {
const event = new CustomEvent("pointerlockmove", { detail: data }); const event = new CustomEvent("pointerlockmove", { detail: data });
window.dispatchEvent(event); window.dispatchEvent(event);
}); });
+2 -3
View File
@@ -1,9 +1,8 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/messages";
export function createLocalizationManager(editor: Editor) { export function createLocalizationManager(editor: Editor) {
// Subscribe to process backend event // Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate", (data) => {
const localized = localizeTimestamp(data.commitDate); const localized = localizeTimestamp(data.commitDate);
editor.handle.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year); editor.handle.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
}); });
+4 -5
View File
@@ -1,12 +1,11 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { DisplayDialogPanic } from "@graphite/messages"; import type { DialogState } from "@graphite/state-providers/dialog";
import { type DialogState } from "@graphite/state-providers/dialog";
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform"; import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
import { stripIndents } from "@graphite/utility-functions/strip-indents"; import { stripIndents } from "@graphite/utility-functions/strip-indents";
export function createPanicManager(editor: Editor, dialogState: DialogState) { export function createPanicManager(editor: Editor, dialogState: DialogState) {
// Code panic dialog and console error // Code panic dialog and console error
editor.subscriptions.subscribeJsMessage(DisplayDialogPanic, (data) => { editor.subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => {
// `Error.stackTraceLimit` is only available in V8/Chromium // `Error.stackTraceLimit` is only available in V8/Chromium
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(Error as any).stackTraceLimit = Infinity; (Error as any).stackTraceLimit = Infinity;
@@ -68,7 +67,7 @@ export function githubUrl(panicDetails: string): string {
if (value) url.searchParams.set(field, value); if (value) url.searchParams.set(field, value);
}); });
return url.toString(); return String(url);
}; };
let urlString = buildUrl(true); let urlString = buildUrl(true);
+61 -40
View File
@@ -1,18 +1,12 @@
import { createStore, del, get, set, update } from "idb-keyval"; import { createStore, del, get, set, update } from "idb-keyval";
import { get as getFromStore } from "svelte/store"; import { get as getFromStore } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { import type { FrontendMessages } from "@graphite/messages";
TriggerPersistenceWriteDocument, import type { PortfolioState } from "@graphite/state-providers/portfolio";
TriggerPersistenceRemoveDocument,
TriggerSavePreferences, type TriggerPersistenceWriteDocument = FrontendMessages["TriggerPersistenceWriteDocument"];
TriggerLoadPreferences, type TriggerSavePreferences = FrontendMessages["TriggerSavePreferences"];
TriggerLoadFirstAutoSaveDocument,
TriggerLoadRestAutoSaveDocuments,
TriggerSaveActiveDocument,
TriggerOpenLaunchDocuments,
} from "@graphite/messages";
import { type PortfolioState } from "@graphite/state-providers/portfolio";
const graphiteStore = createStore("graphite", "store"); const graphiteStore = createStore("graphite", "store");
@@ -33,14 +27,14 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
"documents", "documents",
(old) => { (old) => {
const documents = old || {}; const documents = old || {};
documents[autoSaveDocument.documentId] = autoSaveDocument; documents[String(autoSaveDocument.documentId)] = autoSaveDocument;
return documents; return documents;
}, },
graphiteStore, graphiteStore,
); );
await storeDocumentOrder(); await storeDocumentOrder();
await storeCurrentDocumentId(autoSaveDocument.documentId); await storeCurrentDocumentId(String(autoSaveDocument.documentId));
} }
async function removeDocument(id: string) { async function removeDocument(id: string) {
@@ -79,35 +73,56 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
async function loadFirstDocument() { async function loadFirstDocument() {
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore); 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 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; if (!previouslySavedDocuments || !documentOrder) return;
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
if (currentDocumentId && currentDocumentId in previouslySavedDocuments) { if (currentDocumentId !== undefined && String(currentDocumentId) in previouslySavedDocuments) {
const doc = previouslySavedDocuments[currentDocumentId]; const doc = previouslySavedDocuments[String(currentDocumentId)];
editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false); editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.handle.selectDocument(BigInt(currentDocumentId)); editor.handle.selectDocument(currentDocumentId);
} else { } else {
const len = orderedSavedDocuments.length; const len = orderedSavedDocuments.length;
if (len > 0) { if (len > 0) {
const doc = orderedSavedDocuments[len - 1]; const doc = orderedSavedDocuments[len - 1];
editor.handle.openAutoSavedDocument(BigInt(doc.documentId), doc.details.name, doc.details.isSaved, doc.document, false); editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.handle.selectDocument(BigInt(doc.documentId)); editor.handle.selectDocument(doc.documentId);
} }
} }
} }
async function loadRestDocuments() { async function loadRestDocuments() {
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore); 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 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; if (!previouslySavedDocuments || !documentOrder) return;
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
if (currentDocumentId) { if (currentDocumentId !== undefined) {
const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.documentId === currentDocumentId); const currentIndex = orderedSavedDocuments.findIndex((doc) => doc.documentId === currentDocumentId);
const beforeCurrentIndex = currentIndex - 1; const beforeCurrentIndex = currentIndex - 1;
const afterCurrentIndex = currentIndex + 1; const afterCurrentIndex = currentIndex + 1;
@@ -115,28 +130,25 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
for (let i = beforeCurrentIndex; i >= 0; i--) { for (let i = beforeCurrentIndex; i >= 0; i--) {
const { documentId, document, details } = orderedSavedDocuments[i]; const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details; 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++) { for (let i = afterCurrentIndex; i < orderedSavedDocuments.length; i++) {
const { documentId, document, details } = orderedSavedDocuments[i]; const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details; 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 { } else {
const length = orderedSavedDocuments.length; const length = orderedSavedDocuments.length;
for (let i = length - 2; i >= 0; i--) { for (let i = length - 2; i >= 0; i--) {
const { documentId, document, details } = orderedSavedDocuments[i]; const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details; const { name, isSaved } = details;
editor.handle.openAutoSavedDocument(BigInt(documentId), name, isSaved, document, true); editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, true);
} }
if (length > 0) { if (length > 0) editor.handle.selectDocument(orderedSavedDocuments[length - 1].documentId);
const id = orderedSavedDocuments[length - 1].documentId;
editor.handle.selectDocument(BigInt(id));
}
} }
} }
@@ -154,30 +166,39 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
// FRONTEND MESSAGE SUBSCRIPTIONS // FRONTEND MESSAGE SUBSCRIPTIONS
// Subscribe to process backend events // Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerSavePreferences", async (data) => {
await savePreferences(data.preferences); await savePreferences(data.preferences);
}); });
editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerLoadPreferences", async () => {
await loadPreferences(); await loadPreferences();
}); });
editor.subscriptions.subscribeJsMessage(TriggerPersistenceWriteDocument, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
await storeDocument(data); await storeDocument(data);
}); });
editor.subscriptions.subscribeJsMessage(TriggerPersistenceRemoveDocument, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => {
await removeDocument(data.documentId); await removeDocument(String(data.documentId));
}); });
editor.subscriptions.subscribeJsMessage(TriggerLoadFirstAutoSaveDocument, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument", async () => {
await loadFirstDocument(); await loadFirstDocument();
}); });
editor.subscriptions.subscribeJsMessage(TriggerLoadRestAutoSaveDocuments, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments", async () => {
await loadRestDocuments(); 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 // 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 documentId = String(data.documentId);
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore); 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 (!previouslySavedDocuments) return;
if (documentId in previouslySavedDocuments) { if (documentId in previouslySavedDocuments) {
await storeCurrentDocumentId(documentId); await storeCurrentDocumentId(documentId);
-4
View File
@@ -1,9 +1,5 @@
// This file is the browser's entry point for the JS bundle // 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 { mount } from "svelte";
import App from "@graphite/App.svelte"; import App from "@graphite/App.svelte";
+509 -1452
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,7 +1,7 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { type AppWindowPlatform, UpdatePlatform, UpdateViewportHolePunch, UpdateMaximized, UpdateFullscreen, UpdateUIScale } from "@graphite/messages"; import type { AppWindowPlatform } from "@graphite/messages";
export function createAppWindowState(editor: Editor) { export function createAppWindowState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -13,31 +13,31 @@ export function createAppWindowState(editor: Editor) {
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(UpdatePlatform, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdatePlatform", (data) => {
update((state) => { update((state) => {
state.platform = data.platform; state.platform = data.platform;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateMaximized, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateMaximized", (data) => {
update((state) => { update((state) => {
state.maximized = data.maximized; state.maximized = data.maximized;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateFullscreen, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateFullscreen", (data) => {
update((state) => { update((state) => {
state.fullscreen = data.fullscreen; state.fullscreen = data.fullscreen;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateViewportHolePunch, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateViewportHolePunch", (data) => {
update((state) => { update((state) => {
state.viewportHolePunch = data.active; state.viewportHolePunch = data.active;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateUIScale, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateUIScale", (data) => {
update((state) => { update((state) => {
state.uiScale = data.scale; state.uiScale = data.scale;
return state; return state;
+9 -9
View File
@@ -1,9 +1,9 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { type IconName } from "@graphite/icons"; import type { IconName } from "@graphite/icons";
import { DisplayDialog, DialogClose, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchLayout, TriggerDisplayThirdPartyLicensesDialog } from "@graphite/messages";
import type { Layout } from "@graphite/messages"; import type { Layout } from "@graphite/messages";
import { patchLayout } from "@graphite/utility-functions/widgets";
export function createDialogState(editor: Editor) { export function createDialogState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -45,7 +45,7 @@ export function createDialogState(editor: Editor) {
} }
// Subscribe to process backend events // Subscribe to process backend events
editor.subscriptions.subscribeJsMessage(DisplayDialog, (data) => { editor.subscriptions.subscribeFrontendMessage("DisplayDialog", (data) => {
update((state) => { update((state) => {
state.visible = true; state.visible = true;
@@ -55,30 +55,30 @@ export function createDialogState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (data) => { editor.subscriptions.subscribeLayoutUpdate("DialogButtons", (data) => {
update((state) => { update((state) => {
patchLayout(state.buttons, data); patchLayout(state.buttons, data);
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn1, (data) => { editor.subscriptions.subscribeLayoutUpdate("DialogColumn1", (data) => {
update((state) => { update((state) => {
patchLayout(state.column1, data); patchLayout(state.column1, data);
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn2, (data) => { editor.subscriptions.subscribeLayoutUpdate("DialogColumn2", (data) => {
update((state) => { update((state) => {
patchLayout(state.column2, data); patchLayout(state.column2, data);
return state; 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"; 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.`; 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.`;
+9 -19
View File
@@ -1,19 +1,9 @@
import { tick } from "svelte"; import { tick } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import {
patchLayout,
UpdateDocumentBarLayout,
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateWorkingColorsLayout,
UpdateNodeGraphControlBarLayout,
UpdateGraphViewOverlay,
UpdateGraphFadeArtwork,
} from "@graphite/messages";
import type { Layout } from "@graphite/messages"; import type { Layout } from "@graphite/messages";
import { patchLayout } from "@graphite/utility-functions/widgets";
export function createDocumentState(editor: Editor) { export function createDocumentState(editor: Editor) {
const state = writable({ const state = writable({
@@ -30,13 +20,13 @@ export function createDocumentState(editor: Editor) {
const { subscribe, update } = state; const { subscribe, update } = state;
// Update layouts // Update layouts
editor.subscriptions.subscribeJsMessage(UpdateGraphFadeArtwork, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateGraphFadeArtwork", (data) => {
update((state) => { update((state) => {
state.fadeArtwork = data.percentage; state.fadeArtwork = data.percentage;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, async (data) => { editor.subscriptions.subscribeLayoutUpdate("ToolOptions", async (data) => {
await tick(); await tick();
update((state) => { update((state) => {
@@ -44,7 +34,7 @@ export function createDocumentState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, async (data) => { editor.subscriptions.subscribeLayoutUpdate("DocumentBar", async (data) => {
await tick(); await tick();
update((state) => { update((state) => {
@@ -52,7 +42,7 @@ export function createDocumentState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, async (data) => { editor.subscriptions.subscribeLayoutUpdate("ToolShelf", async (data) => {
await tick(); await tick();
update((state) => { update((state) => {
@@ -60,7 +50,7 @@ export function createDocumentState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateWorkingColorsLayout, async (data) => { editor.subscriptions.subscribeLayoutUpdate("WorkingColors", async (data) => {
await tick(); await tick();
update((state) => { update((state) => {
@@ -68,7 +58,7 @@ export function createDocumentState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphControlBarLayout, (data) => { editor.subscriptions.subscribeLayoutUpdate("NodeGraphControlBar", (data) => {
update((state) => { update((state) => {
patchLayout(state.nodeGraphControlBarLayout, data); patchLayout(state.nodeGraphControlBarLayout, data);
return state; return state;
@@ -76,7 +66,7 @@ export function createDocumentState(editor: Editor) {
}); });
// Show or hide the graph view overlay // Show or hide the graph view overlay
editor.subscriptions.subscribeJsMessage(UpdateGraphViewOverlay, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateGraphViewOverlay", (data) => {
update((state) => { update((state) => {
state.graphViewOverlayOpen = data.open; state.graphViewOverlayOpen = data.open;
return state; return state;
+2 -3
View File
@@ -1,7 +1,6 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { WindowFullscreen } from "@graphite/messages";
export function createFullscreenState(editor: Editor) { export function createFullscreenState(editor: Editor) {
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard // 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(); toggleFullscreen();
}); });
+23 -47
View File
@@ -1,33 +1,9 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { NodeGraphError } from "@graphite/messages"; import type { NodeGraphError, Box, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages";
import {
type Box, type UpdateImportsExports = FrontendMessages["UpdateImportsExports"];
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";
export function createNodeGraphState(editor: Editor) { export function createNodeGraphState(editor: Editor) {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -62,56 +38,56 @@ export function createNodeGraphState(editor: Editor) {
} }
// Set up message subscriptions on creation // Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(SendUIMetadata, (data) => { editor.subscriptions.subscribeFrontendMessage("SendUIMetadata", (data) => {
update((state) => { update((state) => {
state.nodeDescriptions = data.nodeDescriptions; state.nodeDescriptions = new Map(data.nodeDescriptions);
state.nodeTypes = data.nodeTypes; state.nodeTypes = data.nodeTypes;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateBox, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateBox", (data) => {
update((state) => { update((state) => {
state.box = data.box; state.box = data.box;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateClickTargets, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateClickTargets", (data) => {
update((state) => { update((state) => {
state.clickTargets = data.clickTargets; state.clickTargets = data.clickTargets;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateContextMenuInformation, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateContextMenuInformation", (data) => {
update((state) => { update((state) => {
state.contextMenuInformation = data.contextMenuInformation; state.contextMenuInformation = data.contextMenuInformation;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateImportReorderIndex, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateImportReorderIndex", (data) => {
update((state) => { update((state) => {
state.reorderImportIndex = data.importIndex; state.reorderImportIndex = data.importIndex;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateExportReorderIndex, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateExportReorderIndex", (data) => {
update((state) => { update((state) => {
state.reorderExportIndex = data.exportIndex; state.reorderExportIndex = data.exportIndex;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateImportsExports", (data) => {
update((state) => { update((state) => {
state.updateImportsExports = data; state.updateImportsExports = data;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateInSelectedNetwork", (data) => {
update((state) => { update((state) => {
state.inSelectedNetwork = data.inSelectedNetwork; state.inSelectedNetwork = data.inSelectedNetwork;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateLayerWidths", (data) => {
update((state) => { update((state) => {
state.layerWidths = data.layerWidths; state.layerWidths = data.layerWidths;
state.chainWidths = data.chainWidths; state.chainWidths = data.chainWidths;
@@ -119,7 +95,7 @@ export function createNodeGraphState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphNodes", (data) => {
update((state) => { update((state) => {
state.nodes.clear(); state.nodes.clear();
data.nodes.forEach((node) => { data.nodes.forEach((node) => {
@@ -128,19 +104,19 @@ export function createNodeGraphState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphErrorDiagnostic, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic", (data) => {
update((state) => { update((state) => {
state.error = data.error; state.error = data.error;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateVisibleNodes", (data) => {
update((state) => { update((state) => {
state.visibleNodes = new Set<bigint>(data.nodes); state.visibleNodes = new Set<bigint>(data.nodes);
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphWires, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphWires", (data) => {
update((state) => { update((state) => {
data.wires.forEach((wireUpdate) => { data.wires.forEach((wireUpdate) => {
let inputMap = state.wires.get(wireUpdate.id); let inputMap = state.wires.get(wireUpdate.id);
@@ -158,31 +134,31 @@ export function createNodeGraphState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(ClearAllNodeGraphWires, () => { editor.subscriptions.subscribeFrontendMessage("ClearAllNodeGraphWires", () => {
update((state) => { update((state) => {
state.wires.clear(); state.wires.clear();
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphSelection", (data) => {
update((state) => { update((state) => {
state.selected = data.selected; state.selected = data.selected;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphTransform, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => {
update((state) => { update((state) => {
state.transform = data.transform; state.transform = data.transform;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateNodeThumbnail, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateNodeThumbnail", (data) => {
update((state) => { update((state) => {
state.thumbnails.set(data.id, data.value); state.thumbnails.set(data.id, data.value);
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateWirePathInProgress, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateWirePathInProgress", (data) => {
update((state) => { update((state) => {
state.wirePathInProgress = data.wirePath; state.wirePathInProgress = data.wirePath;
return state; return state;
+13 -26
View File
@@ -1,20 +1,7 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { OpenDocument } from "@graphite/messages"; 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 { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
import { rasterizeSVG } from "@graphite/utility-functions/rasterization"; import { rasterizeSVG } from "@graphite/utility-functions/rasterization";
@@ -29,13 +16,13 @@ export function createPortfolioState(editor: Editor) {
}); });
// Set up message subscriptions on creation // Set up message subscriptions on creation
editor.subscriptions.subscribeJsMessage(UpdateOpenDocumentsList, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateOpenDocumentsList", (data) => {
update((state) => { update((state) => {
state.documents = data.openDocuments; state.documents = data.openDocuments;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateActiveDocument, (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {
update((state) => { update((state) => {
// Assume we receive a correct document id // Assume we receive a correct document id
const activeId = state.documents.findIndex((doc) => doc.id === data.documentId); const activeId = state.documents.findIndex((doc) => doc.id === data.documentId);
@@ -43,7 +30,7 @@ export function createPortfolioState(editor: Editor) {
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(TriggerFetchAndOpenDocument, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerFetchAndOpenDocument", async (data) => {
try { try {
const url = new URL(`demo-artwork/${data.filename}`, document.location.href); const url = new URL(`demo-artwork/${data.filename}`, document.location.href);
const response = await fetch(url); const response = await fetch(url);
@@ -55,22 +42,22 @@ export function createPortfolioState(editor: Editor) {
}, 0); }, 0);
} }
}); });
editor.subscriptions.subscribeJsMessage(TriggerOpen, async () => { editor.subscriptions.subscribeFrontendMessage("TriggerOpen", async () => {
const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data"); const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data");
editor.handle.openFile(data.filename, data.content); 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 // 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"); const data = await upload("image/*", "data");
editor.handle.importFile(data.filename, data.content); editor.handle.importFile(data.filename, data.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerSaveDocument, (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerSaveDocument", (data) => {
downloadFile(data.name, data.content); downloadFile(data.name, data.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerSaveFile, (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerSaveFile", (data) => {
downloadFile(data.name, data.content); downloadFile(data.name, data.content);
}); });
editor.subscriptions.subscribeJsMessage(TriggerExportImage, async (data) => { editor.subscriptions.subscribeFrontendMessage("TriggerExportImage", async (data) => {
const { svg, name, mime, size } = 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) // 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 // Rasterize the SVG to an image file
try { 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 // Have the browser download the file to the user's disk
downloadFileBlob(name, blob); 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 // 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) => { update((state) => {
state.dataPanelOpen = data.open; state.dataPanelOpen = data.open;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdatePropertiesPanelState, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdatePropertiesPanelState", async (data) => {
update((state) => { update((state) => {
state.propertiesPanelOpen = data.open; state.propertiesPanelOpen = data.open;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelState, async (data) => { editor.subscriptions.subscribeFrontendMessage("UpdateLayersPanelState", async (data) => {
update((state) => { update((state) => {
state.layersPanelOpen = data.open; state.layersPanelOpen = data.open;
return state; return state;
+5 -5
View File
@@ -1,7 +1,7 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { SendShortcutAltClick, SendShortcutFullscreen, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages"; import type { ActionShortcut } from "@graphite/messages";
import { operatingSystem } from "@graphite/utility-functions/platform"; import { operatingSystem } from "@graphite/utility-functions/platform";
const SHOW_TOOLTIP_DELAY_MS = 500; const SHOW_TOOLTIP_DELAY_MS = 500;
@@ -65,19 +65,19 @@ export function createTooltipState(editor: Editor) {
document.addEventListener("keydown", closeTooltip); document.addEventListener("keydown", closeTooltip);
document.addEventListener("wheel", closeTooltip); document.addEventListener("wheel", closeTooltip);
editor.subscriptions.subscribeJsMessage(SendShortcutShiftClick, async (data) => { editor.subscriptions.subscribeFrontendMessage("SendShortcutShiftClick", async (data) => {
update((state) => { update((state) => {
state.shiftClickShortcut = data.shortcut; state.shiftClickShortcut = data.shortcut;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => { editor.subscriptions.subscribeFrontendMessage("SendShortcutAltClick", async (data) => {
update((state) => { update((state) => {
state.altClickShortcut = data.shortcut; state.altClickShortcut = data.shortcut;
return state; return state;
}); });
}); });
editor.subscriptions.subscribeJsMessage(SendShortcutFullscreen, async (data) => { editor.subscriptions.subscribeFrontendMessage("SendShortcutFullscreen", async (data) => {
update((state) => { update((state) => {
state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut; state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut;
return state; return state;
+48 -48
View File
@@ -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 // 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() { 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>) => { const subscribeFrontendMessage = <T extends keyof FrontendMessages>(messageType: T, callback: (data: FrontendMessages[T]) => void) => {
subscriptions[messageType.name] = callback; subscriptions[messageType] = callback;
}; };
const unsubscribeJsMessage = <T extends JsMessage>(messageType: new () => T) => { const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => {
delete subscriptions[messageType.name]; delete subscriptions[messageType];
}; };
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => { const subscribeLayoutUpdate = (target: LayoutTarget, callback: (diffs: WidgetDiff[]) => void) => {
// 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 layoutCallbacks[target] = callback;
const messageMaker = messageMakers[messageType]; };
if (!messageMaker) {
// eslint-disable-next-line no-console const unsubscribeLayoutUpdate = (target: LayoutTarget) => {
console.error( delete layoutCallbacks[target];
`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`.)",
); const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
return; // 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`. // Try to execute the callback. Due to message ordering, the callback may not be registered yet,
const isJsMessageMaker = (fn: typeof messageMaker): fn is typeof JsMessage => "jsMessageMarker" in fn; // so we retry a few times on the next stack frame to give onMount a chance to run.
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.
let retries = 0; let retries = 0;
const callCallback = () => { const callCallback = () => {
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler const callback = getCallback();
const callback = subscriptions[message.constructor.name];
// 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) { if (callback) {
callback(message); callback(callbackData);
} else if (retries <= 3) { } else if (retries <= 3) {
retries += 1; retries += 1;
setTimeout(callCallback, 0); setTimeout(callCallback, 0);
} else { } else {
// eslint-disable-next-line no-console // 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 { return {
subscribeJsMessage, subscribeFrontendMessage,
unsubscribeJsMessage, unsubscribeFrontendMessage,
handleJsMessage, subscribeLayoutUpdate,
unsubscribeLayoutUpdate,
handleFrontendMessage,
}; };
} }
export type SubscriptionRouter = ReturnType<typeof createSubscriptionRouter>; export type SubscriptionRouter = ReturnType<typeof createSubscriptionRouter>;
+257
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import { extractPixelData } from "@graphite/utility-functions/rasterization"; import { extractPixelData } from "@graphite/utility-functions/rasterization";
export function downloadFileURL(filename: string, url: string) { export function downloadFileURL(filename: string, url: string) {
@@ -10,7 +10,7 @@ export function panicProxy<T extends object>(module: T): T {
if (!isFunction) return targetValue; if (!isFunction) return targetValue;
// Special handling to wrap the return of a constructor in the proxy // 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) { if (isClass) {
return function (...args: unknown[]): unknown { 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) // 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 -1
View File
@@ -1,4 +1,4 @@
import { type Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
let resizeObserver: ResizeObserver | undefined; let resizeObserver: ResizeObserver | undefined;
+131
View File
@@ -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");
}
-1
View File
@@ -5,7 +5,6 @@
"moduleResolution": "node", "moduleResolution": "node",
"strict": true, "strict": true,
"importHelpers": true, "importHelpers": true,
"experimentalDecorators": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
+2 -1
View File
@@ -4,7 +4,8 @@ import path from "path";
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from "@sveltejs/vite-plugin-svelte";
import { sveltePreprocess } from "svelte-preprocess"; import { sveltePreprocess } from "svelte-preprocess";
import { defineConfig, type PluginOption } from "vite"; import { defineConfig } from "vite";
import type { PluginOption } from "vite";
import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-assets"; import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-assets";
const projectRootDir = path.resolve(__dirname); const projectRootDir = path.resolve(__dirname);
@@ -44,5 +44,5 @@ Frontend-to-backend communication is achieved through a thin Rust translation la
Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up each [`FrontendMessage`](https://github.com/GraphiteEditor/Graphite/tree/master/editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JavaScript data structures in [`/frontend/src/messages.ts`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/messages.ts). Various TS code subscribes to these messages by calling: Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up each [`FrontendMessage`](https://github.com/GraphiteEditor/Graphite/tree/master/editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JavaScript data structures in [`/frontend/src/messages.ts`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/messages.ts). Various TS code subscribes to these messages by calling:
```rs ```rs
subscribeJsMessage(MessageName, (messageData) => { /* callback code */ }); subscribeFrontendMessage(MessageName, (messageData) => { /* callback code */ });
``` ```
+1
View File
@@ -79,6 +79,7 @@ export default defineConfig([
"@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }], "@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }],
// Import plugin config (for intelligently validating module import statements) // Import plugin config (for intelligently validating module import statements)
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"import/no-unresolved": "error", "import/no-unresolved": "error",
"import/prefer-default-export": "off", "import/prefer-default-export": "off",
"import/no-relative-packages": "error", "import/no-relative-packages": "error",
@@ -19,7 +19,7 @@ function initializeImageComparison() {
const leaveHandler = (/** @type {PointerEvent} **/ event) => { const leaveHandler = (/** @type {PointerEvent} **/ event) => {
moveHandler(event); moveHandler(event);
const randomCode = Math.random().toString().substring(2); const randomCode = String(Math.random()).substring(2);
element.dataset.lastInteraction = randomCode; element.dataset.lastInteraction = randomCode;
setTimeout(() => { setTimeout(() => {