Add frontend file structure docs and some related cleanup

This commit is contained in:
Keavon Chambers 2022-05-23 19:13:51 -07:00
parent e4e37cca7b
commit d5b43ef2da
21 changed files with 168 additions and 145 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
target/
*.spv
*.exrc
website/public

View file

@ -3,7 +3,7 @@
"[rust]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "matklad.rust-analyzer",
"editor.defaultFormatter": "rust-lang.rust-analyzer",
},
// Web: save on format
"[typescript][javascript][vue]": {
@ -13,6 +13,11 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
},
// Handlebars: don't save on format
// (`about.hbs` is used by Cargo About to encode license information)
"[handlebars]": {
"editor.formatOnSave": false,
},
// Rust Analyzer config
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
"rust-analyzer.checkOnSave.command": "clippy",

View file

@ -1,5 +1,7 @@
# Graphite project documentation
WARNING: A lot of the information in these documents are out of date.
## [Graphite editor manual](editor/README.md)
Click above for the Graphite editor manual. This will evolve into the official manual for the Graphite editor. It contains documentation for each feature of Graphite focused on both usage instructions and technical details.

View file

@ -1,27 +1,3 @@
# Graphite codebase docs
This is a great place to start learning about the Graphite codebase and its architecture and code structure.
## Core libraries
Graphite's core rust codebase is split into two reusable libraries:
- Editor
- Graphene
The Editor depends on Graphene, but Graphene can also be used alone. These are used internally but also intended for usage by third parties through Rust or linked by a project in C, C++, or another language.
## Code structure
The main modules of the project architecture are outlined below. Some parts describe future features and the directories don't exist yet. **Bold** modules are required for Graphite 0.1 which is purely an SVG editor.
- **Graphite Editor (Frontend)**: `/frontend/`
Prototype web-based GUI for Graphite that will eventually be replaced by a native GUI frontend implementation
- **Vue web app**: `src/`
Imports the WASM code and uses Vue props to customize and reuse most GUI components
- **Rust WebAssembly translation layer**: `wasm/`
Wraps the editor client backend and provides an API for the web app to use unburdened by Rust's complex data types that are not supported by JS
- **Graphite Editor (Backend)**: `/editor/`
Used by a frontend editor client to maintain GUI state and dispatch user events. The official Graphite editor is the primary user, but others software like game engines could embed their own customized editor implementations. Depends on Graphene.
- **Graphene (Document Graph Engine)**: `/graphene/`
A stateless library for updating Graphite design document (GDD) files. The official Graphite CLI and editor client backend are the primary users, but this library is intended to be useful to any application that wants to link the library for the purpose of updating GDD files by sending edit operations. This also serves as the 2D render engine and this is intended to be useful to any application that wants to link the Graphene library for the purpose of rendering Graphite graphs. For example, games can link the library and render procedural textures with customizable parametric input values.
Until this is written, please check the [contributing guide](https://graphite.rs/contribute) and the README.md files in individual directories of the project structure.

View file

@ -37,7 +37,7 @@
## User stories
Contributions welcome! If you think of something Graphite would be great for, submit a pull request or send Keavon a message on Graphite's Discord server.
Contributions welcome! If you think of something Graphite would be great for, submit a pull request or send Keavon a message on Graphite's Discord server to add it here.
## Photography
- Using a face detection node to sort photos into the correct folders upon export
@ -91,5 +91,5 @@ Contributions welcome! If you think of something Graphite would be great for, su
- Running on a server to let users upload images for a custom T-shirt printing website, and it renders their graphic on the models shirt (or other custom printing online stores)
- Generating a PDF invoice based on data in a pipeline in a server
## Industrial control
## Computer vision and industrial control
- Factory line is examining its fruit for defects. In order to verify the quality, they need to enhance the contrast, automatically orient the image and correct the lighting. They then pass the results into a machine learning algorithm to classify, and sometimes need to snoop on the stream of data to manually do quality control (ImageMagick or custom Python libraries are often used for this right now)

View file

@ -1,5 +1,7 @@
# Inputs and keybindings
WARNING: This document is out of date and some of the information contained herein does not accurately explain the current status or plans.
## Input categories
- Keyboard

3
frontend/.gitignore vendored
View file

@ -1,4 +1,3 @@
node_modules/
dist/
wasm/pkg/
rust-licenses.js
pkg/

34
frontend/README.md Normal file
View file

@ -0,0 +1,34 @@
# Overview of `/frontend/`
The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Vue](https://vuejs.org/) framework. The backend is written in Rust and compiled to WebAssembly (WASM) to be run in the browser alongside the JS code.
For lack of other options, the frontend is currently written as a web app. Maintaining web compatibility will always be a requirement, but the long-term plan is to port this code to a Rust-based native GUI framework, either written by the Rust community or created by our project if necessary. As a medium-term compromise, we may wrap the web-based frontend in a desktop webview windowing solution like Electron (probably not) or [Tauri](https://tauri.studio/) (probably).
## Bundled assets: `assets/`
Icons and images that are used in components and embedded into the application bundle by the build system using [loaders](https://webpack.js.org/loaders/).
## Public assets: `public/`
Static content like favicons that are copied directly into the root of the build output by the build system.
## Vue/TypeScript source: `src/`
Source code for the web app in the form of Vue components and [TypeScript](https://www.typescriptlang.org/) files.
## WebAssembly wrapper: `wasm/`
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the WASM module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack).
## ESLint configurations: `.eslintrc.js`
[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Vue files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Vue support, [Airbnb](https://github.com/airbnb/javascript)'s popular catalog of sane defaults, and [Prettier](https://prettier.io/)'s role as a code formatter.
## npm ecosystem packages: `package.json`
While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js and then run `cd frontend && npm install` to install these packages on your system. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vue CLI, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process.
## npm package installed versions: `package-lock.json`
Specifies the exact versions of packages installed in the npm dependency tree. While `package.json` specifies which packages to install and their minimum/maximum acceptable version numbers, `package-lock.json` represents the exact versions of each dependency and sub-dependency. Running `npm install` will grab these exact versions to ensure you are using the same packages as everyone else working on Graphite. `npm update` will modify `package-lock.json` to specify newer versions of any updated (sub-)dependencies and download those, as long as they don't exceed the maximum version allowed in `package.json`. To check for newer versions that exceed the max version, run `npm outdated` to see a list. Unless you know why you are doing it, try to avoid committing updates to `package-lock.json` by mistake if your code changes don't pertain to package updates. And never manually modify the file.
## TypeScript configurations: `tsconfig.json`
Basic configuration options for the TypeScript build tool to do its job in our repository.
## vue-svg-loader.js
An extremely simple Webpack loader that allows us to `import` SVG files into our JS to be used like they are Vue components. They end up as inline SVG elements in the web page like `<svg ...>...</svg>`, rather than being `<img src="..." />` elements, which provides some benefits like being able to apply CSS styles to them. These get embedded into the bundle (they live somewhere all together in a big, messy JS file) rather than being separate static SVG files that would have to be served individually.
## Vue CLI/Webpack configurations: `vue.config.js`
[Vue CLI](https://cli.vuejs.org/) is a command line tool built around the [Webpack](https://webpack.js.org/) bundler/build system. This file is where we configure Webpack to set up plugins (like wasm-pack and license-checker) and loaders (like for Vue and SVG files). Part of the license-checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code.

View file

@ -1,29 +1,24 @@
# Overview of `/frontend/src/`
## Vue components: `components/`
Vue components that build the Graphite editor GUI, which are mounted in `App.vue`. These are Vue SFCs (single-file components) which each contain a Vue-templated HTML section, an SCSS (Stylus CSS) section, and a script section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
## I/O managers: `io-managers/`
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend events to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/api.rs`).
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend events to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`).
Each I/O manager is a self-contained module where one instance is created in `App.vue` when it's mounted to the DOM at app startup.
During development when HMR (hot-module replacement) occurs, these are also unmounted to clean up after themselves, so they can be mounted again with the updated code. Therefore, any side-effects that these managers cause (e.g. adding event listeners to the page) need a destructor function that cleans them up. The destructor function, when applicable, is returned by the module and automatically called in `App.vue` on unmount.
## State providers: `state-providers/`
TypeScript files which provide reactive state and importable functions to Vue components. Each module defines a Vue reactive state object `const state = reactive({ ... });` and exports this from the module in the returned object as the key-value pair `state: readonly(state) as typeof state,` using Vue's `readonly()` wrapper. Other functions may also be defined in the module and exported after `state`, which provide a way for Vue components to call functions to manipulate the state.
In `App.vue`, an instance of each of these are given to Vue's [`provide()`](https://vuejs.org/api/application.html#app-provide) function. This allows any component to access the state provider instance by specifying it in its `inject: [...]` array. The state is accessed in a component with `this.stateProviderName.state.someReactiveVariable` and any exposed functions are accessed with `this.stateProviderName.state.someExposedVariable()`. They can also be used in the Vue HTML template (sans the `this.` prefix).
## *I/O managers vs. state providers*
*Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to `inject`ed by components to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Vue components.*
*Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to `inject`ed by components to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Vue components.*
## Utility functions: `utility-functions/`
TypeScript files which define and `export` individual helper functions for use elsewhere in the codebase. These files should not persist state outside each function.
## WASM communication: `wasm-communication/`
@ -31,23 +26,18 @@ TypeScript files which define and `export` individual helper functions for use e
TypeScript files which serve as the JS interface to the WASM bindings for the editor backend.
### WASM editor: `editor.ts`
Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings module provided by wasm-bindgen/wasm-pack. It is stored in a local variable and can be retrieved with the `getWasmInstance()` function. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below).
Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below).
`initWasm()` occurs in `main.ts` right before the Vue application exists, then `createEditor()` is run in `App.vue` during the Vue app's creation. Similarly to the state providers described above, the editor is `provide`d so other components can `inject` it and call functions on `this.editor.raw`, `this.editor.instance`, or `this.editor.subscriptions`.
### Message definitions: `messages.ts`
Defines the message formats and data types received from the backend. Since Rust and JS support different styles of data representation, this bridges the gap from Rust into JS land. Messages (and the data contained within) are serialized in Rust by `serde` into JSON, and these definitions are manually kept up-to-date to parallel the message structs and their data types. (However, directives like `#[serde(skip)]` or `#[serde(rename = "someOtherName")]` may cause the TypeScript format to look slightly different from the Rust structs.) These definitions are basically just for the sake of TypeScript to understand the format, although in some cases we may perform data conversion here using translation functions that we can provide.
### 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`.
## Vue app: `App.vue`
The entry point for the Vue application. This is where we define global CSS style rules, construct the editor,construct/destruct the editor and I/O managers, and construct/provide state providers.
## Entry point: `main.ts`
The entry point for the entire project. Here we simply initialize the WASM module with `await initWasm();` then initialize the Vue application with `createApp(App).mount("#app");`.

View file

@ -139,14 +139,14 @@ function makeEntries(editor: Editor): MenuListEntries {
{
label: "Raise To Front",
shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"],
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.raw.i32_max()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.instance.i32_max()),
},
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(-1) },
{
label: "Lower to Back",
shortcut: ["KeyControl", "KeyShift", "KeyRightBracket"],
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.raw.i32_min()),
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.instance.i32_min()),
},
],
],

View file

@ -1,5 +1,5 @@
import { PortfolioState } from "@/state-providers/portfolio";
import { Editor, getWasmInstance } from "@/wasm-communication/editor";
import { Editor } from "@/wasm-communication/editor";
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument } from "@/wasm-communication/messages";
const GRAPHITE_INDEXED_DB_VERSION = 2;
@ -75,7 +75,7 @@ export async function createPersistenceManager(editor: Editor, portfolio: Portfo
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
const currentDocumentVersion = getWasmInstance().graphite_version();
const currentDocumentVersion = editor.instance.graphite_document_version();
orderedSavedDocuments.forEach((doc: TriggerIndexedDbWriteDocument) => {
if (doc.version === currentDocumentVersion) {
editor.instance.open_auto_saved_document(BigInt(doc.details.id), doc.details.name, doc.details.is_saved, doc.document);

View file

@ -23,7 +23,7 @@ export function createPortfolioState(editor: Editor) {
state.activeDocumentIndex = activeId;
});
editor.subscriptions.subscribeJsMessage(TriggerFileUpload, async () => {
const extension = editor.raw.file_save_suffix();
const extension = editor.instance.file_save_suffix();
const data = await upload(extension);
editor.instance.open_document_file(data.filename, data.content);
});

View file

@ -25,16 +25,18 @@ export async function initWasm(): Promise<void> {
// Should be called after running `initWasm()` and its promise resolving
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createEditor() {
// Functions from `api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen)
const raw: WasmRawInstance = getWasmInstance();
// Raw: Object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen)
if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup");
const raw: WasmRawInstance = wasmImport;
// Functions from `api.rs` that are part of the editor instance (generated by wasm-bindgen)
const instance: WasmEditorInstance = new raw.JsEditorHandle(invokeJsMessageSubscription);
function invokeJsMessageSubscription(messageType: JsMessageType, data: Record<string, unknown>): void {
subscriptions.handleJsMessage(messageType, data, raw, instance);
}
// Instance: Object containing many functions from `editor_api.rs` that are part of the editor instance (generated by wasm-bindgen)
const instance: WasmEditorInstance = new raw.JsEditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>): void => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper editor instance
// We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, instance);
});
// 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
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
return {
@ -43,8 +45,3 @@ export function createEditor() {
subscriptions,
};
}
export function getWasmInstance(): WasmRawInstance {
if (wasmImport) return wasmImport;
throw new Error("Editor WASM backend was not initialized at application startup");
}

View file

@ -5,35 +5,6 @@ const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const LicenseCheckerWebpackPlugin = require("license-checker-webpack-plugin");
function generateRustLicenses() {
console.info("Generating license information for rust code");
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
cwd: path.join(__dirname, ".."),
encoding: "utf8",
timeout: 60000, // one minute
shell: true,
windowsHide: true, // hide the DOS window on windows
});
if (status !== 0) {
if (status !== 101) {
// cargo returns 101 when the subcommand wasn't found
console.error("cargo-about failed", status, stderr);
}
return null;
}
// Make sure the output starts as expected, we don't want to eval an error message.
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
console.error("Unexpected output from cargo-about", stdout);
return null;
}
// Security-wise, eval() isn't any worse than require(), but it doesn't need a temporary file.
// eslint-disable-next-line no-eval
return eval(stdout);
}
process.env.VUE_APP_COMMIT_DATE = execSync("git log -1 --format=%cd", { encoding: "utf-8" }).trim();
process.env.VUE_APP_COMMIT_HASH = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
process.env.VUE_APP_COMMIT_BRANCH = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
@ -206,6 +177,35 @@ ${license.licenseText}
return formattedLicenseNotice;
}
function generateRustLicenses() {
console.info("Generating license information for rust code");
const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], {
cwd: path.join(__dirname, ".."),
encoding: "utf8",
timeout: 60000, // one minute
shell: true,
windowsHide: true, // hide the DOS window on windows
});
if (status !== 0) {
if (status !== 101) {
// cargo returns 101 when the subcommand wasn't found
console.error("cargo-about failed", status, stderr);
}
return null;
}
// Make sure the output starts as expected, we don't want to eval an error message.
if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) {
console.error("Unexpected output from cargo-about", stdout);
return null;
}
// Security-wise, eval() isn't any worse than require(), but it doesn't need a temporary file.
// eslint-disable-next-line no-eval
return eval(stdout);
}
function htmlDecode(input) {
if (!input) return input;

14
frontend/wasm/README.md Normal file
View file

@ -0,0 +1,14 @@
# Overview of `/frontend/wasm/`
## WASM wrapper API: `src/editor_api.rs`
Provides bindings for JS to call functions defined in this file, and for FrontendMessages to be sent from Rust back to JS in the form of a callback to the subscription router. This WASM wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send FrontendMessages back to JS.
## WASM wrapper helper code: `src/helpers.rs`
Assorted function and struct definitions used in the WASM wrapper.
## WASM wrapper initialization: `src/lib.rs`
Entry point for the Rust entire codebase in the WASM environment. Initializes the WASM module and persistent storage for editor and WASM wrapper instances.
## WASM wrapper tests: `tests/`
We currently have no WASM wrapper tests, but this is where they would go.

View file

@ -19,23 +19,39 @@ use serde_wasm_bindgen::{self, from_value};
use std::sync::atomic::Ordering;
use wasm_bindgen::prelude::*;
/// Set the random seed used by the editor by calling this from JS upon initialization.
/// This is necessary because WASM doesn't have a random number generator.
#[wasm_bindgen]
pub fn set_random_seed(seed: u64) {
editor::communication::set_uuid_seed(seed);
}
/// Access a handle to WASM memory
#[wasm_bindgen]
pub fn wasm_memory() -> JsValue {
wasm_bindgen::memory()
}
// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell we must make all methods take a non mutable reference to self.
// Not doing this creates an issue when rust calls into JS which calls back to rust in the same call stack.
#[wasm_bindgen]
#[derive(Clone)]
pub struct JsEditorHandle {
editor_id: u64,
handle_response: js_sys::Function,
frontend_message_handler_callback: js_sys::Function,
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
impl JsEditorHandle {
#[wasm_bindgen(constructor)]
pub fn new(handle_response: js_sys::Function) -> Self {
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor_id = generate_uuid();
let editor = Editor::new();
let editor_handle = JsEditorHandle { editor_id, handle_response };
let editor_handle = JsEditorHandle {
editor_id,
frontend_message_handler_callback,
};
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor));
JS_EDITOR_HANDLES.with(|instances| instances.borrow_mut().insert(editor_id, editor_handle.clone()));
editor_handle
@ -48,27 +64,27 @@ impl JsEditorHandle {
return;
}
let responses = EDITOR_INSTANCES.with(|instances| {
let frontend_messages = EDITOR_INSTANCES.with(|instances| {
instances
.borrow_mut()
.get_mut(&self.editor_id)
.expect("EDITOR_INSTANCES does not contain the current editor_id")
.handle_message(message.into())
});
for response in responses.into_iter() {
for message in frontend_messages.into_iter() {
// Send each FrontendMessage to the JavaScript frontend
self.handle_response(response);
self.send_frontend_message_to_js(message);
}
}
// Sends a FrontendMessage to JavaScript
fn handle_response(&self, message: FrontendMessage) {
fn send_frontend_message_to_js(&self, message: FrontendMessage) {
let message_type = message.to_discriminant().local_name();
let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true);
let message_data = message.serialize(&serializer).expect("Failed to serialize FrontendMessage");
let js_return_value = self.handle_response.call2(&JsValue::null(), &JsValue::from(message_type), &message_data);
let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data);
if let Err(error) = js_return_value {
log::error!(
@ -111,6 +127,30 @@ impl JsEditorHandle {
EDITOR_HAS_CRASHED.load(Ordering::SeqCst)
}
/// Get the constant `FILE_SAVE_SUFFIX`
#[wasm_bindgen]
pub fn file_save_suffix(&self) -> String {
FILE_SAVE_SUFFIX.into()
}
/// Get the constant `GRAPHITE_DOCUMENT_VERSION`
#[wasm_bindgen]
pub fn graphite_document_version(&self) -> String {
GRAPHITE_DOCUMENT_VERSION.to_string()
}
/// Get the constant `i32::MAX`
#[wasm_bindgen]
pub fn i32_max(&self) -> i32 {
i32::MAX
}
/// Get the constant `i32::MIN`
#[wasm_bindgen]
pub fn i32_min(&self) -> i32 {
i32::MIN
}
/// Request that the Node Graph panel be shown or hidden by toggling the visibility state
pub fn toggle_node_graph_visibility(&self) {
self.dispatch(WorkspaceMessage::NodeGraphToggleVisibility);
@ -491,8 +531,8 @@ impl JsEditorHandle {
// Needed to make JsEditorHandle functions pub to Rust.
// The reason is not fully clear but it has to do with the #[wasm_bindgen] procedural macro.
impl JsEditorHandle {
pub fn handle_response_rust_proxy(&self, message: FrontendMessage) {
self.handle_response(message);
pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) {
self.send_frontend_message_to_js(message);
}
}
@ -501,40 +541,3 @@ impl Drop for JsEditorHandle {
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().remove(&self.editor_id));
}
}
/// Set the random seed used by the editor by calling this from JS upon initialization.
/// This is necessary because WASM doesn't have a random number generator.
#[wasm_bindgen]
pub fn set_random_seed(seed: u64) {
editor::communication::set_uuid_seed(seed)
}
/// Access a handle to WASM memory
#[wasm_bindgen]
pub fn wasm_memory() -> JsValue {
wasm_bindgen::memory()
}
/// Get the constant `FILE_SAVE_SUFFIX`
#[wasm_bindgen]
pub fn file_save_suffix() -> String {
FILE_SAVE_SUFFIX.into()
}
/// Get the constant `GRAPHITE_DOCUMENT_VERSION`
#[wasm_bindgen]
pub fn graphite_version() -> String {
GRAPHITE_DOCUMENT_VERSION.to_string()
}
/// Get the constant `i32::MAX`
#[wasm_bindgen]
pub fn i32_max() -> i32 {
i32::MAX
}
/// Get the constant `i32::MIN`
#[wasm_bindgen]
pub fn i32_min() -> i32 {
i32::MIN
}

View file

@ -13,7 +13,7 @@ pub fn panic_hook(info: &panic::PanicInfo) {
log::error!("{}", info);
JS_EDITOR_HANDLES.with(|instances| {
instances.borrow_mut().values_mut().for_each(|instance| {
instance.handle_response_rust_proxy(FrontendMessage::DisplayDialogPanic {
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
panic_info: panic_info.clone(),
title: title.clone(),
description: description.clone(),

View file

@ -1,7 +1,8 @@
pub mod api;
pub mod editor_api;
pub mod helpers;
use helpers::{panic_hook, WasmLog};
use std::cell::RefCell;
use std::collections::HashMap;
use std::panic;
@ -13,7 +14,7 @@ pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
pub static LOGGER: WasmLog = WasmLog;
thread_local! {
pub static EDITOR_INSTANCES: RefCell<HashMap<u64, editor::Editor>> = RefCell::new(HashMap::new());
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, api::JsEditorHandle>> = RefCell::new(HashMap::new());
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, editor_api::JsEditorHandle>> = RefCell::new(HashMap::new());
}
/// Initialize the backend

View file

@ -1,8 +1,8 @@
#![cfg(target_arch = "wasm32")]
// #![cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
// use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
// wasm_bindgen_test_configure!(run_in_browser);
// #[wasm_bindgen_test]
// fn pass() {

1
website/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
public/

View file

@ -57,7 +57,7 @@ The Editor's frontend web code lives in `/frontend/src` and the backend Rust cod
### Frontend/backend communication
Frontend (JS) -> backend (Rust/wasm) communication is achieved through a thin Rust translation layer in `/frontend/wasm/api.rs` which wraps the Editor backend's complex Rust data type API and provides the JS with a simpler API of callable functions. These wrapper functions are compiled by wasm-bindgen into autogenerated JS functions that serve as an entry point into the wasm.
Frontend (JS) -> backend (Rust/wasm) communication is achieved through a thin Rust translation layer in `/frontend/wasm/editor_api.rs` which wraps the Editor backend's complex Rust data type API and provides the JS with a simpler API of callable functions. These wrapper functions are compiled by wasm-bindgen into autogenerated JS functions that serve as an entry point into the wasm.
Backend (Rust) -> frontend (JS) communication happens by sending a queue of messages to the frontend message dispatcher. After the JS calls any wrapper API function to get into backend (Rust) code execution, the Editor's business logic runs and queues up `FrontendMessage`s (defined in `editor/src/frontend/frontend_message_handler.rs`) which get mapped from Rust to JS-friendly data types in `frontend/src/dispatcher/js-messages.ts`. Various JS code subscribes to these messages by calling `subscribeJsMessage(MessageName, (messageData) => { /* callback code */ });`.