Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "cargo install cargo-watch wasm-pack cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
"onCreateCommand": "cargo install cargo-watch cargo-about && cargo install -f wasm-bindgen-cli@0.2.121",
"customizations": {
"vscode": {
// NOTE: Keep this in sync with `.vscode/extensions.json`
Expand Down
37 changes: 25 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,20 @@ jobs:
RUSTC_WRAPPER: /usr/bin/sccache
CARGO_INCREMENTAL: 0
SCCACHE_DIR: /var/lib/github-actions/.cache
WASM_BINDGEN_CLI_VERSION: "0.2.121"
BINARYEN_VERSION: "130"
steps:
- name: 📥 Clone repository
uses: actions/checkout@v6
with:
repository: ${{ inputs.checkout_repo || github.repository }}
ref: ${{ inputs.checkout_ref || '' }}

- name: 🗑 Clear wasm-bindgen cache
run: rm -r ~/.cache/.wasm-pack || true

- name: 🟢 Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc

- name: 🚧 Install build dependencies
run: |
cd frontend
npm run setup

- name: 🦀 Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
Expand All @@ -87,6 +81,19 @@ jobs:
rustflags: ""
target: wasm32-unknown-unknown

- name: 🚧 Install wasm-bindgen-cli and Binaryen wasm-opt
run: |
if ! wasm-bindgen --version 2>/dev/null | grep -qF "$WASM_BINDGEN_CLI_VERSION"; then
cargo install -f "wasm-bindgen-cli@$WASM_BINDGEN_CLI_VERSION"
fi

BINARYEN_DIR="$HOME/.cache/binaryen-version_$BINARYEN_VERSION"
if [ ! -x "$BINARYEN_DIR/bin/wasm-opt" ]; then
mkdir -p "$BINARYEN_DIR"
curl -sSfL "https://github.com/WebAssembly/binaryen/releases/download/version_$BINARYEN_VERSION/binaryen-version_$BINARYEN_VERSION-x86_64-linux.tar.gz" | tar xz -C "$BINARYEN_DIR" --strip-components=1
fi
echo "$BINARYEN_DIR/bin" >> "$GITHUB_PATH"

- name: 🔀 Choose production deployment environment and insert template
id: production-env
if: github.event_name == 'push'
Expand Down Expand Up @@ -294,6 +301,7 @@ jobs:

env:
WASM_BINDGEN_CLI_VERSION: "0.2.121"
BINARYEN_VERSION: "130"

steps:
- name: 📥 Clone repository
Expand Down Expand Up @@ -341,13 +349,15 @@ jobs:
winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements
winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements
winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements
winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements
winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements

"OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append
"PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append

cargo binstall --no-confirm --force wasm-pack
curl.exe -sSfL -o "$env:RUNNER_TEMP\binaryen.tar.gz" "https://github.com/WebAssembly/binaryen/releases/download/version_$env:BINARYEN_VERSION/binaryen-version_$env:BINARYEN_VERSION-x86_64-windows.tar.gz"
tar -xzf "$env:RUNNER_TEMP\binaryen.tar.gz" -C $env:RUNNER_TEMP
"$env:RUNNER_TEMP\binaryen-version_$env:BINARYEN_VERSION\bin" | Out-File -FilePath $env:GITHUB_PATH -Append

cargo binstall --no-confirm --force cargo-about
cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION"

Expand Down Expand Up @@ -485,6 +495,7 @@ jobs:

env:
WASM_BINDGEN_CLI_VERSION: "0.2.121"
BINARYEN_VERSION: "130"

steps:
- name: 📥 Clone repository
Expand Down Expand Up @@ -529,15 +540,17 @@ jobs:
brew install \
pkg-config \
openssl@3 \
binaryen \
llvm \
cargo-binstall

echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH

cargo binstall --no-confirm --force wasm-pack
curl -sSfL -o "$RUNNER_TEMP/binaryen.tar.gz" "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-arm64-macos.tar.gz"
tar -xzf "$RUNNER_TEMP/binaryen.tar.gz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/binaryen-version_${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"

cargo binstall --no-confirm --force cargo-about
cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}"

Expand Down
1 change: 0 additions & 1 deletion .nix/dev.nix
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ pkgs.mkShell (
pkgs.nodejs
pkgs.binaryen
pkgs.wasm-bindgen-cli_0_2_121
pkgs.wasm-pack
pkgs.cargo-about

pkgs.rustc
Expand Down
2 changes: 0 additions & 2 deletions .nix/pkgs/graphite.nix
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ deps.crane.lib.buildPackage (
pkgs.nodejs
pkgs.binaryen
pkgs.wasm-bindgen-cli_0_2_121
pkgs.wasm-pack
pkgs.cargo-about
pkgs.removeReferencesTo
pkgs.importNpmLock.npmConfigHook
Expand All @@ -94,7 +93,6 @@ deps.crane.lib.buildPackage (
npmRoot = "${info.src}/frontend";
};
npmRoot = "frontend";
npmConfigScript = "setup";
makeCacheWritable = true;

env = {
Expand Down
12 changes: 12 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use crate::messages::prelude::*;
use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::transform::Footprint;
Expand Down Expand Up @@ -235,6 +237,16 @@ pub enum DocumentMessage {
UpdateVectorData {
vector_data: HashMap<NodeId, Arc<Vector>>,
},
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
#[serde(skip)]
UpdateFillAttributes {
fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
},
#[serde(skip)]
UpdateStrokeAttributes {
stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
},
Undo,
UngroupSelectedLayers,
UngroupLayer {
Expand Down
45 changes: 43 additions & 2 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,34 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
.collect();
self.network_interface.update_vector_data(layer_vector_data);
}
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_fill_attributes = fill_attributes
.into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, attrs)
})
})
.collect();
self.network_interface.update_fill_attributes(layer_fill_attributes);
}
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_stroke_attributes = stroke_attributes
.into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, attrs)
})
})
.collect();
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
}
DocumentMessage::Undo => {
if self.network_interface.transaction_status() != TransactionStatus::Finished {
return;
Expand Down Expand Up @@ -2486,10 +2514,23 @@ impl DocumentMessageHandler {
continue;
};

let has_fill = !matches!(style.fill, Fill::None);
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);

let has_fill = if let Some(list) = fill_graphic_list {
list.element(0).is_some()
} else {
!matches!(style.fill, Fill::None)
};
// `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color.
// So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke.
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke());
// `ATTR_STROKE` is the source of truth when set; fall back to `style.stroke.color` only when no attribute is present.
let stroke_visible = if let Some(list) = stroke_graphic_list {
list.element(0).is_some_and(|g| !g.is_fully_transparent())
} else {
style.stroke.as_ref().and_then(|s| s.color()).is_some_and(|c| c.a() != 0.)
};
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;

// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
if !has_stroke {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
Expand Down Expand Up @@ -39,6 +41,12 @@ pub struct DocumentMetadata {
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
/// This provides accurate SegmentIds for layers without explicit Path nodes.
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
/// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint
/// information that lives on the list.
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read
/// stroke paint information that lives on the list.
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graphene_std::ContextDependencies;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::transform::Footprint;
Expand Down Expand Up @@ -3401,6 +3403,16 @@ impl NodeNetworkInterface {
pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>) {
self.document_metadata.layer_vector_data = new_layer_vector_data;
}

/// Update the per-layer `ATTR_FILL` snapshot.
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes;
}

/// Update the per-layer `ATTR_STROKE` snapshot.
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
}
}

// Public mutable methods
Expand Down
4 changes: 4 additions & 0 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@ impl NodeGraphExecutor {
text_frames,
clip_targets,
vector_data,
fill_attributes,
stroke_attributes,
backgrounds: _,
} = render_output.metadata;

Expand All @@ -460,6 +462,8 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
responses.add(DocumentMessage::UpdateVectorData { vector_data });
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes });
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);
Expand Down
2 changes: 1 addition & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
node_modules/
wasm/pkg/
wrapper/pkg/
public/build/
dist/
2 changes: 1 addition & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Source code for the web app in the form of Svelte components and [TypeScript](ht

## Editor wrapper: `wrapper/`

Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, 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).
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, 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`. As part of `cargo run`, our build tool compiles this crate to Wasm, runs the `wasm-bindgen` CLI to generate the JS/TS bindings in `wrapper/pkg/`, and (for release builds) optimizes the binary with Binaryen's `wasm-opt`.

## ESLint configuration: `eslint.config.js`

Expand Down
11 changes: 7 additions & 4 deletions frontend/package-installer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This script automatically installs the npm packages listed in package-lock.json and runs before `npm start`.
// This script automatically installs the npm packages listed in package-lock.json and runs as part of `npm run setup` (invoked by `cargo run`).
// It skips the installation if this has already run and neither package.json nor package-lock.json has been modified since.

import { execSync } from "child_process";
Expand All @@ -11,7 +11,8 @@ const isInstallNeeded = () => {
if (!existsSync(INSTALL_TIMESTAMP_FILE)) return true;

const timestamp = statSync(INSTALL_TIMESTAMP_FILE).mtime;
return ["package.json", "package-lock.json"].some((file) => {
// This script is itself included so that changes to the install process below cause a reinstall
return ["package.json", "package-lock.json", "package-installer.js"].some((file) => {
return existsSync(file) && statSync(file).mtime > timestamp;
});
};
Expand All @@ -22,8 +23,10 @@ if (isInstallNeeded()) {
// eslint-disable-next-line no-console
console.log("Installing npm packages...");

// Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks
execSync("npm ci --prefer-offline --no-audit --no-fund", { stdio: "inherit" });
// Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks.
// The devDependencies are explicitly included because they hold the build tooling (Vite, etc.), which npm would
// otherwise omit in environments that set NODE_ENV=production (like CI does for the sake of the Vite build).
execSync("npm ci --include=dev --prefer-offline --no-audit --no-fund", { stdio: "inherit" });

// Touch the install timestamp file
writeFileSync(INSTALL_TIMESTAMP_FILE, "");
Expand Down
18 changes: 1 addition & 17 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,9 @@
"browserslist": "> 1.5%, last 2 versions, not dead, not ie 11, not op_mini all, not ios_saf < 13",
"type": "module",
"scripts": {
"---------- DEV SERVER ----------": "",
"start": "npm run setup && npm run wasm:build-dev && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-dev\"",
"production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"",
"---------- BUILDS ----------": "",
"build": "npm run setup && npm run wasm:build-production && vite build",
"build-dev": "npm run setup && npm run wasm:build-dev && vite build --mode dev",
"build-native": "npm run setup && npm run native:build-production",
"build-native-dev": "npm run setup && npm run native:build-dev",
"---------- UTILITIES ----------": "",
"check": "svelte-check --fail-on-warnings && eslint",
"fix": "eslint --fix",
"---------- INTERNAL ----------": "",
"setup": "node package-installer.js && node branding-installer.js",
"native:build-dev": "wasm-pack build ./wrapper --dev --target=web --no-default-features --features native && vite build --mode native",
"native:build-production": "wasm-pack build ./wrapper --release --target=web --no-default-features --features native && vite build --mode native",
"wasm:build-dev": "wasm-pack build ./wrapper --dev --target=web",
"wasm:build-production": "wasm-pack build ./wrapper --release --target=web",
"wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wrapper --shell \"wasm-pack build . --dev --target=web -- --color=always\"",
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wrapper --shell \"wasm-pack build . --release --target=web -- --color=always\""
"setup": "node package-installer.js && node branding-installer.js"
},
"//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders 1px above its intended position.",
"///": "Waiting on <https://github.com/import-js/eslint-plugin-import/issues/3227> before we can update @eslint/js and eslint to 10.x",
Expand Down
16 changes: 0 additions & 16 deletions frontend/wrapper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,6 @@ ron = { workspace = true }
serde_json = { workspace = true }
node-macro = { workspace = true }

[package.metadata.wasm-pack.profile.dev]
wasm-opt = false

[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
debug-js-glue = true
demangle-name-section = true
dwarf-debug-info = false

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Os", "-g"]

[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
demangle-name-section = false
dwarf-debug-info = false

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(wasm_bindgen_unstable_test_coverage)',
Expand Down
3 changes: 3 additions & 0 deletions node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
convert_node!(from: List<NodeId>, to: AttributeValueDyn),
convert_node!(from: List<Color>, to: AttributeValueDyn),
convert_node!(from: List<GradientStops>, to: AttributeValueDyn),
convert_node!(from: List<Vector>, to: AttributeValueDyn),
convert_node!(from: List<Raster<CPU>>, to: AttributeValueDyn),
convert_node!(from: List<Raster<GPU>>, to: AttributeValueDyn),
convert_node!(from: List<Graphic>, to: AttributeValueDyn),
// into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>),
#[cfg(feature = "gpu")]
Expand Down
Loading
Loading