Merge branch 'master' into merge_point

This commit is contained in:
Adesh Gupta
2025-08-31 09:35:19 +05:30
committed by GitHub
64 changed files with 2466 additions and 2137 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ jobs:
- name: 🧪 Run Rust tests - name: 🧪 Run Rust tests
run: | run: |
mold -run cargo test --all-features --workspace mold -run cargo test --all-features
- name: 📃 Generate code documentation info for website - name: 📃 Generate code documentation info for website
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
+42 -4
View File
@@ -34,11 +34,47 @@
inherit system overlays; inherit system overlays;
}; };
rustc-wasm = pkgs.rust-bin.stable.latest.default.override { rustExtensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
rust = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ]; targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; extensions = rustExtensions;
}; };
rustGPUToolchainPkg = pkgs.rust-bin.nightly."2025-06-23".default.override {
extensions = rustExtensions ++ [ "rustc-dev" "llvm-tools" ];
};
rustGPUToolchainRustPlatform = pkgs.makeRustPlatform {
cargo = rustGPUToolchainPkg;
rustc = rustGPUToolchainPkg;
};
rustc_codegen_spirv = rustGPUToolchainRustPlatform.buildRustPackage (finalAttrs: {
pname = "rustc_codegen_spirv";
version = "0-unstable-2025-08-04";
src = pkgs.fetchFromGitHub {
owner = "Rust-GPU";
repo = "rust-gpu";
rev = "c12f216121820580731440ee79ebc7403d6ea04f";
hash = "sha256-rG1cZvOV0vYb1dETOzzbJ0asYdE039UZImobXZfKIno=";
};
cargoHash = "sha256-AEigcEc5wiBd3zLqWN/2HSbkfOVFneAqNvg9HsouZf4=";
cargoBuildFlags = [ "-p" "rustc_codegen_spirv" "--features=use-compiled-tools" "--no-default-features" ];
doCheck = false;
});
rustGpuCargo = pkgs.writeShellScriptBin "cargo" ''
#!${pkgs.lib.getExe pkgs.bash}
filtered_args=()
for arg in "$@"; do
case "$arg" in
+nightly|+nightly-*) ;;
*) filtered_args+=("$arg") ;;
esac
done
exec ${rustGPUToolchainPkg}/bin/cargo ${"\${filtered_args[@]}"}
'';
rustGpuPathOverride = "${rustGpuCargo}/bin:${rustGPUToolchainPkg}/bin";
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: { libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
version = "139.0.17"; version = "139.0.17";
gitRevision = "6c347eb"; gitRevision = "6c347eb";
@@ -51,7 +87,6 @@
strip $out/lib/* strip $out/lib/*
''; '';
}); });
libcefPath = pkgs.runCommand "libcef-path" {} '' libcefPath = pkgs.runCommand "libcef-path" {} ''
mkdir -p $out mkdir -p $out
@@ -85,7 +120,7 @@
# Development tools that don't need to be in LD_LIBRARY_PATH # Development tools that don't need to be in LD_LIBRARY_PATH
buildTools = [ buildTools = [
rustc-wasm rust
pkgs.nodejs pkgs.nodejs
pkgs.nodePackages.npm pkgs.nodePackages.npm
pkgs.binaryen pkgs.binaryen
@@ -119,6 +154,9 @@
CEF_PATH = libcefPath; CEF_PATH = libcefPath;
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS"; XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
RUST_GPU_PATH_OVERRIDE = rustGpuPathOverride;
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
shellHook = '' shellHook = ''
alias cargo='mold --run cargo' alias cargo='mold --run cargo'
''; '';
+4
View File
@@ -33,6 +33,10 @@
}, },
// Rust Analyzer config // Rust Analyzer config
"rust-analyzer.cargo.allTargets": false, "rust-analyzer.cargo.allTargets": false,
"rust-analyzer.procMacro.ignored": {
"serde_derive": ["Serialize", "Deserialize"],
"specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387
},
// ESLint config // ESLint config
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.workingDirectories": ["./frontend", "./website"], "eslint.workingDirectories": ["./frontend", "./website"],
Generated
+572 -579
View File
File diff suppressed because it is too large Load Diff
+45 -33
View File
@@ -1,43 +1,55 @@
[workspace] [workspace]
members = [ members = [
"editor",
"desktop", "desktop",
"desktop/wrapper", "desktop/wrapper",
"proc-macros", "desktop/embedded-resources",
"editor",
"frontend/wasm", "frontend/wasm",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/gapplication-io", "node-graph/gapplication-io",
"node-graph/gbrush", "node-graph/gbrush",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gcore-shaders", "node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes", "node-graph/gmath-nodes",
"node-graph/gpath-bool", "node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/graster-nodes", "node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer", "node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
"node-graph/preprocessor", "node-graph/preprocessor",
"node-graph/wgpu-executor",
"proc-macros",
]
default-members = [
"desktop",
"desktop/wrapper",
"editor",
"frontend/wasm",
"libraries/dyn-any", "libraries/dyn-any",
"libraries/path-bool", "libraries/path-bool",
"libraries/math-parser", "libraries/math-parser",
] "node-graph/gapplication-io",
default-members = [
"editor",
"frontend/wasm",
"node-graph/gbrush", "node-graph/gbrush",
"node-graph/gcore", "node-graph/gcore",
"node-graph/gcore-shaders", "node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes", "node-graph/gmath-nodes",
"node-graph/gpath-bool", "node-graph/gpath-bool",
"node-graph/graph-craft", "node-graph/graph-craft",
"node-graph/graphene-cli", "node-graph/graphene-cli",
"node-graph/graster-nodes", "node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer", "node-graph/gsvg-renderer",
"node-graph/interpreted-executor", "node-graph/interpreted-executor",
"node-graph/node-macro", "node-graph/node-macro",
"node-graph/preprocessor",
"node-graph/wgpu-executor",
# blocked by https://github.com/rust-lang/cargo/issues/15890
# "proc-macros",
] ]
resolver = "2" resolver = "2"
@@ -80,16 +92,16 @@ env_logger = "0.11"
log = "0.4" log = "0.4"
bitflags = { version = "2.4", features = ["serde"] } bitflags = { version = "2.4", features = ["serde"] }
ctor = "0.2" ctor = "0.2"
convert_case = "0.7" convert_case = "0.8"
derivative = "2.2" derivative = "2.2"
thiserror = "2" thiserror = "2"
anyhow = "1.0" anyhow = "1.0"
proc-macro2 = { version = "1", features = ["span-locations"] } proc-macro2 = { version = "1", features = ["span-locations"] }
quote = "1.0" quote = "1.0"
chrono = "0.4" chrono = "0.4"
ron = "0.8" ron = "0.11"
fastnoise-lite = "1.1" fastnoise-lite = "1.1"
wgpu = { version = "25.0.2", features = [ wgpu = { version = "25.0", features = [
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm # We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
"fragile-send-sync-non-atomic-wasm", "fragile-send-sync-non-atomic-wasm",
"spirv", "spirv",
@@ -120,9 +132,9 @@ web-sys = { version = "=0.3.77", features = [
winit = { version = "0.30", features = ["wayland", "rwh_06"] } winit = { version = "0.30", features = ["wayland", "rwh_06"] }
url = "2.5" url = "2.5"
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] } tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
vello = { git = "https://github.com/linebender/vello.git" } # TODO switch back to stable when a release is made vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made
resvg = "0.44" resvg = "0.45"
usvg = "0.44" usvg = "0.45"
rand = { version = "0.9", default-features = false, features = ["std_rng"] } rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9" rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = [ glam = { version = "0.29", default-features = false, features = [
@@ -136,9 +148,9 @@ image = { version = "0.25", default-features = false, features = [
"jpeg", "jpeg",
"bmp", "bmp",
] } ] }
parley = "0.5.0" parley = "0.5"
skrifa = "0.32.0" skrifa = "0.36"
pretty_assertions = "1.4.1" pretty_assertions = "1.4"
fern = { version = "0.7", features = ["colored"] } fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7" num_enum = "0.7"
num-derive = "0.4" num-derive = "0.4"
@@ -159,25 +171,25 @@ syn = { version = "2.0", default-features = false, features = [
"extra-traits", "extra-traits",
"proc-macro", "proc-macro",
] } ] }
kurbo = { version = "0.11.0", features = ["serde"] } kurbo = { version = "0.11", features = ["serde"] }
lyon_geom = "1.0" lyon_geom = "1.0"
petgraph = { version = "0.7.1", default-features = false, features = [ petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
"graphmap", half = { version = "2.4", default-features = false, features = ["bytemuck"] }
] }
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1", features = ["std"] } tinyvec = { version = "1", features = ["std"] }
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.7", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" } iai-callgrind = { version = "0.16" }
ndarray = "0.16.1" ndarray = "0.16"
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.27", features = ["derive"] }
dirs = "6.0" dirs = "6.0"
cef = "139.0.1" cef = "139.0"
include_dir = "0.7.4" include_dir = "0.7"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1.41" tracing = "0.1"
rfd = "0.15.4" rfd = "0.15"
open = "5.3.2" open = "5.3"
poly-cool = "0.2.0" poly-cool = "0.3"
spin = "0.10"
clap = "4.5"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
@@ -41,9 +41,7 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [ ignore = [
"RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error` "RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error`
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative` "RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
"RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring`
"RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste` "RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste`
"RUSTSEC-2025-0014", # Unmaintained but still fully functional crate `humantime`
] ]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score # Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories # lower than the range specified will be ignored. Note that ignored advisories
+5 -3
View File
@@ -9,7 +9,9 @@ edition = "2024"
rust-version = "1.87" rust-version = "1.87"
[features] [features]
default = ["gpu", "accelerated_paint"] default = ["recommended", "embedded_resources"]
recommended = ["gpu", "accelerated_paint"]
embedded_resources = ["dep:graphite-desktop-embedded-resources"]
gpu = ["graphite-desktop-wrapper/gpu"] gpu = ["graphite-desktop-wrapper/gpu"]
# Hardware acceleration features # Hardware acceleration features
@@ -19,15 +21,15 @@ accelerated_paint_d3d11 = ["windows", "ash"]
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"] accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
[dependencies] [dependencies]
# # Local dependencies # Local dependencies
graphite-desktop-wrapper = { path = "wrapper" } graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
wgpu = { workspace = true } wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] } winit = { workspace = true, features = ["serde"] }
thiserror = { workspace = true } thiserror = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
cef = { workspace = true } cef = { workspace = true }
include_dir = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
dirs = { workspace = true } dirs = { workspace = true }
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "graphite-desktop-embedded-resources"
version = "0.1.0"
description = "Graphite Desktop Embedded Resources"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
edition = "2024"
rust-version = "1.87"
[dependencies]
include_dir = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] }
+17
View File
@@ -0,0 +1,17 @@
const RESOURCES: &str = "../../frontend/dist";
// Check if the directory `RESOURCES` exists and sets the embedded_resources cfg accordingly
// Absolute path of `RESOURCES` available via the `EMBEDDED_RESOURCES` environment variable
fn main() {
let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:rerun-if-changed={RESOURCES}");
if let Ok(resources) = crate_dir.join(RESOURCES).canonicalize()
&& resources.exists()
{
println!("cargo:rustc-cfg=embedded_resources");
println!("cargo:rustc-env=EMBEDDED_RESOURCES={}", resources.to_string_lossy());
} else {
println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?");
}
}
+10
View File
@@ -0,0 +1,10 @@
//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary.
//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate.
//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly.
//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation.
#[cfg(embedded_resources)]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES"));
#[cfg(not(embedded_resources))]
pub static EMBEDDED_RESOURCES: Option<include_dir::Dir> = None;
+94 -14
View File
@@ -16,6 +16,9 @@
use crate::CustomEvent; use crate::CustomEvent;
use crate::render::FrameBufferRef; use crate::render::FrameBufferRef;
use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message}; use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message};
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::PathBuf;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
@@ -27,7 +30,6 @@ mod input;
mod internal; mod internal;
mod ipc; mod ipc;
mod platform; mod platform;
mod scheme_handler;
mod utility; mod utility;
#[cfg(feature = "accelerated_paint")] #[cfg(feature = "accelerated_paint")]
@@ -38,11 +40,12 @@ use texture_import::SharedTextureHandle;
pub(crate) use context::{CefContext, CefContextBuilder, InitError}; pub(crate) use context::{CefContext, CefContextBuilder, InitError};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
pub(crate) trait CefEventHandler: Clone { pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
fn window_size(&self) -> WindowSize; fn window_size(&self) -> WindowSize;
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>); fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
#[cfg(feature = "accelerated_paint")] #[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle); fn draw_gpu(&self, shared_texture: SharedTextureHandle);
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
/// Scheudule the main event loop to run the cef event loop after the timeout /// Scheudule the main event loop to run the cef event loop after the timeout
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation. /// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant); fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
@@ -62,12 +65,34 @@ impl WindowSize {
} }
} }
#[derive(Clone)]
pub(crate) struct Resource {
pub(crate) reader: ResourceReader,
pub(crate) mimetype: Option<String>,
}
#[expect(dead_code)]
#[derive(Clone)]
pub(crate) enum ResourceReader {
Embedded(Cursor<&'static [u8]>),
File(Arc<File>),
}
impl Read for ResourceReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
ResourceReader::Embedded(cursor) => cursor.read(buf),
ResourceReader::File(file) => file.as_ref().read(buf),
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct CefHandler { pub(crate) struct CefHandler {
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>, window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
event_loop_proxy: EventLoopProxy<CustomEvent>, event_loop_proxy: EventLoopProxy<CustomEvent>,
wgpu_context: WgpuContext, wgpu_context: WgpuContext,
} }
struct WindowSizeReceiver { struct WindowSizeReceiver {
receiver: Receiver<WindowSize>, receiver: Receiver<WindowSize>,
window_size: WindowSize, window_size: WindowSize,
@@ -142,6 +167,73 @@ impl CefEventHandler for CefHandler {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture)); let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
} }
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to import shared texture: {}", e);
}
}
}
fn load_resource(&self, path: PathBuf) -> Option<Resource> {
let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path };
let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") {
"html" => Some("text/html".to_string()),
"css" => Some("text/css".to_string()),
"txt" => Some("text/plain".to_string()),
"wasm" => Some("application/wasm".to_string()),
"js" => Some("application/javascript".to_string()),
"png" => Some("image/png".to_string()),
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
"svg" => Some("image/svg+xml".to_string()),
"xml" => Some("application/xml".to_string()),
"json" => Some("application/json".to_string()),
"ico" => Some("image/x-icon".to_string()),
"woff" => Some("font/woff".to_string()),
"woff2" => Some("font/woff2".to_string()),
"ttf" => Some("font/ttf".to_string()),
"otf" => Some("font/otf".to_string()),
"webmanifest" => Some("application/manifest+json".to_string()),
"graphite" => Some("application/graphite+json".to_string()),
_ => None,
};
#[cfg(feature = "embedded_resources")]
{
if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES
&& let Some(file) = resources.get_file(&path)
{
return Some(Resource {
reader: ResourceReader::Embedded(Cursor::new(file.contents())),
mimetype,
});
}
}
#[cfg(not(feature = "embedded_resources"))]
{
use std::path::Path;
let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?;
let asset_path = Path::new(&asset_path_env);
let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path));
if file_path.exists() && file_path.is_file() {
if let Ok(file) = std::fs::File::open(file_path) {
return Some(Resource {
reader: ResourceReader::File(file.into()),
mimetype,
});
}
}
}
None
}
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) { fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time)); let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
} }
@@ -157,16 +249,4 @@ impl CefEventHandler for CefHandler {
}; };
let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message)); let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message));
} }
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle) {
match shared_texture.import_texture(&self.wgpu_context.device) {
Ok(texture) => {
let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture));
}
Err(e) => {
tracing::error!("Failed to import shared texture: {}", e);
}
}
}
} }
+2 -2
View File
@@ -1,2 +1,2 @@
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static"; pub(crate) const RESOURCE_SCHEME: &str = "resources";
pub(crate) const FRONTEND_DOMAIN: &str = "frontend"; pub(crate) const RESOURCE_DOMAIN: &str = "resources";
+17 -12
View File
@@ -6,20 +6,21 @@ use cef::{
use super::CefContext; use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext; use super::singlethreaded::SingleThreadedCefContext;
use crate::cef::CefHandler; use crate::cef::CefEventHandler;
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME}; use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::cef::dirs::{cef_cache_dir, cef_data_dir}; use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
use crate::cef::input::InputState; use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl}; use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
pub(crate) struct CefContextBuilder { pub(crate) struct CefContextBuilder<H: CefEventHandler> {
pub(crate) args: Args, pub(crate) args: Args,
pub(crate) is_sub_process: bool, pub(crate) is_sub_process: bool,
_marker: std::marker::PhantomData<H>,
} }
unsafe impl Send for CefContextBuilder {} unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}
impl CefContextBuilder { impl<H: CefEventHandler> CefContextBuilder<H> {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let _loader = { let _loader = {
@@ -34,7 +35,11 @@ impl CefContextBuilder {
let switch = CefString::from("type"); let switch = CefString::from("type");
let is_sub_process = cmd.has_switch(Some(&switch)) == 1; let is_sub_process = cmd.has_switch(Some(&switch)) == 1;
Self { args, is_sub_process } Self {
args,
is_sub_process,
_marker: std::marker::PhantomData,
}
} }
pub(crate) fn is_sub_process(&self) -> bool { pub(crate) fn is_sub_process(&self) -> bool {
@@ -45,7 +50,7 @@ impl CefContextBuilder {
let cmd = self.args.as_cmd_line().unwrap(); let cmd = self.args.as_cmd_line().unwrap();
let switch = CefString::from("type"); let switch = CefString::from("type");
let process_type = CefString::from(&cmd.switch_value(Some(&switch))); let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = RenderProcessAppImpl::app(); let mut app = RenderProcessAppImpl::<H>::app();
let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut()); let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 { if ret >= 0 {
SetupError::SubprocessFailed(process_type.to_string()) SetupError::SubprocessFailed(process_type.to_string())
@@ -55,7 +60,7 @@ impl CefContextBuilder {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> { pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings { let settings = Settings {
windowless_rendering_enabled: 1, windowless_rendering_enabled: 1,
multi_threaded_message_loop: 0, multi_threaded_message_loop: 0,
@@ -71,7 +76,7 @@ impl CefContextBuilder {
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: CefHandler) -> Result<impl CefContext, InitError> { pub(crate) fn initialize(self, event_handler: H) -> Result<impl CefContext, InitError> {
let settings = Settings { let settings = Settings {
windowless_rendering_enabled: 1, windowless_rendering_enabled: 1,
multi_threaded_message_loop: 1, multi_threaded_message_loop: 1,
@@ -97,7 +102,7 @@ impl CefContextBuilder {
Ok(super::multithreaded::MultiThreadedCefContextProxy) Ok(super::multithreaded::MultiThreadedCefContextProxy)
} }
fn initialize_inner(self, event_handler: &CefHandler, settings: Settings) -> Result<(), InitError> { fn initialize_inner(self, event_handler: &H, settings: Settings) -> Result<(), InitError> {
let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone())); let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone()));
let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut()); let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults // Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
@@ -113,11 +118,11 @@ impl CefContextBuilder {
} }
} }
fn create_browser(event_handler: CefHandler) -> Result<SingleThreadedCefContext, InitError> { fn create_browser<H: CefEventHandler>(event_handler: H) -> Result<SingleThreadedCefContext, InitError> {
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone())); let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone())); let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str()); let url = CefString::from(format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/").as_str());
let window_info = WindowInfo { let window_info = WindowInfo {
windowless_rendering_enabled: 1, windowless_rendering_enabled: 1,
+4
View File
@@ -2,10 +2,14 @@ mod browser_process_app;
mod browser_process_client; mod browser_process_client;
mod browser_process_handler; mod browser_process_handler;
mod browser_process_life_span_handler; mod browser_process_life_span_handler;
mod render_process_app; mod render_process_app;
mod render_process_handler; mod render_process_handler;
mod render_process_v8_handler; mod render_process_v8_handler;
mod resource_handler;
mod scheme_handler_factory;
pub(super) mod render_handler; pub(super) mod render_handler;
pub(super) mod task; pub(super) mod task;
@@ -5,11 +5,9 @@ use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t}; use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp}; use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
use crate::cef::CefEventHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use super::browser_process_handler::BrowserProcessHandlerImpl; use super::browser_process_handler::BrowserProcessHandlerImpl;
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> { pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>, object: *mut RcImpl<_cef_app_t, Self>,
@@ -30,7 +28,7 @@ impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
} }
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar); SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
} }
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) { fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
@@ -4,9 +4,9 @@ use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t}; use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t};
use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler}; use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler; use crate::cef::CefEventHandler;
use crate::cef::consts::GRAPHITE_SCHEME; use crate::cef::consts::RESOURCE_SCHEME;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> { pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<cef_browser_process_handler_t, Self>, object: *mut RcImpl<cef_browser_process_handler_t, Self>,
@@ -23,7 +23,11 @@ impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> { impl<H: CefEventHandler + Clone> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
fn on_context_initialized(&self) { fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(Some(&CefString::from(GRAPHITE_SCHEME)), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new()))); cef::register_scheme_handler_factory(
Some(&CefString::from(RESOURCE_SCHEME)),
None,
Some(&mut SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(self.event_handler.clone()))),
);
} }
fn on_schedule_message_pump_work(&self, delay_ms: i64) { fn on_schedule_message_pump_work(&self, delay_ms: i64) {
+1 -1
View File
@@ -9,7 +9,6 @@ pub(crate) struct RenderHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_render_handler_t, Self>, object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H, event_handler: H,
} }
impl<H: CefEventHandler> RenderHandlerImpl<H> { impl<H: CefEventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> Self { pub(crate) fn new(event_handler: H) -> Self {
Self { Self {
@@ -18,6 +17,7 @@ impl<H: CefEventHandler> RenderHandlerImpl<H> {
} }
} }
} }
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> { impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) { fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect { if let Some(rect) = rect {
@@ -3,13 +3,14 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp}; use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
use super::render_process_handler::RenderProcessHandlerImpl; use super::render_process_handler::RenderProcessHandlerImpl;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory; use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct RenderProcessAppImpl { pub(crate) struct RenderProcessAppImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_app_t, Self>, object: *mut RcImpl<_cef_app_t, Self>,
render_process_handler: RenderProcessHandler, render_process_handler: RenderProcessHandler,
} }
impl RenderProcessAppImpl { impl<H: CefEventHandler> RenderProcessAppImpl<H> {
pub(crate) fn app() -> App { pub(crate) fn app() -> App {
App::new(Self { App::new(Self {
object: std::ptr::null_mut(), object: std::ptr::null_mut(),
@@ -18,9 +19,9 @@ impl RenderProcessAppImpl {
} }
} }
impl ImplApp for RenderProcessAppImpl { impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar); SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
} }
fn render_process_handler(&self) -> Option<RenderProcessHandler> { fn render_process_handler(&self) -> Option<RenderProcessHandler> {
@@ -32,7 +33,7 @@ impl ImplApp for RenderProcessAppImpl {
} }
} }
impl Clone for RenderProcessAppImpl { impl<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
unsafe { unsafe {
let rc_impl = &mut *self.object; let rc_impl = &mut *self.object;
@@ -44,7 +45,7 @@ impl Clone for RenderProcessAppImpl {
} }
} }
} }
impl Rc for RenderProcessAppImpl { impl<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t { fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe { unsafe {
let base = &*self.object; let base = &*self.object;
@@ -52,7 +53,7 @@ impl Rc for RenderProcessAppImpl {
} }
} }
} }
impl WrapApp for RenderProcessAppImpl { impl<H: CefEventHandler> WrapApp for RenderProcessAppImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object; self.object = object;
} }
@@ -5,7 +5,6 @@ use crate::cef::ipc::{MessageType, SendMessage};
pub struct BrowserProcessV8HandlerImpl { pub struct BrowserProcessV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
} }
impl BrowserProcessV8HandlerImpl { impl BrowserProcessV8HandlerImpl {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() } Self { object: std::ptr::null_mut() }
@@ -72,7 +71,6 @@ impl Clone for BrowserProcessV8HandlerImpl {
Self { object: self.object } Self { object: self.object }
} }
} }
impl Rc for BrowserProcessV8HandlerImpl { impl Rc for BrowserProcessV8HandlerImpl {
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t { fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
unsafe { unsafe {
@@ -81,7 +79,6 @@ impl Rc for BrowserProcessV8HandlerImpl {
} }
} }
} }
impl WrapV8Handler for BrowserProcessV8HandlerImpl { impl WrapV8Handler for BrowserProcessV8HandlerImpl {
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) { fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
self.object = object; self.object = object;
@@ -0,0 +1,108 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler};
use std::cell::RefCell;
use std::ffi::c_int;
use std::io::Read;
use crate::cef::{Resource, ResourceReader};
pub(crate) struct ResourceHandlerImpl {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
reader: Option<RefCell<ResourceReader>>,
mimetype: Option<String>,
}
impl ResourceHandlerImpl {
pub fn new(resource: Option<Resource>) -> Self {
if let Some(resource) = resource {
Self {
object: std::ptr::null_mut(),
reader: Some(resource.reader.into()),
mimetype: resource.mimetype,
}
} else {
Self {
object: std::ptr::null_mut(),
reader: None,
mimetype: None,
}
}
}
}
impl ImplResourceHandler for ResourceHandlerImpl {
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
if let Some(handle_request) = handle_request {
*handle_request = 1;
}
1
}
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
if let Some(response_length) = response_length {
*response_length = -1; // Indicating that the length is unknown
}
if let Some(response) = response {
if self.reader.is_some() {
if let Some(mimetype) = &self.mimetype {
let cef_mime = CefString::from(mimetype.as_str());
response.set_mime_type(Some(&cef_mime));
} else {
response.set_mime_type(None);
}
response.set_status(200);
} else {
response.set_status(404);
response.set_mime_type(Some(&CefString::from("text/plain")));
}
}
}
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
let Some(bytes_read) = bytes_read else { unreachable!() };
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
if let Some(reader) = &self.reader {
if let Ok(read) = reader.borrow_mut().read(out) {
*bytes_read = read as i32;
if read > 0 {
return 1; // Indicating that data was read
}
} else {
*bytes_read = -2; // Indicating ERR_FAILED
}
}
0 // Indicating no data was read
}
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}
impl Clone for ResourceHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
reader: self.reader.clone(),
mimetype: self.mimetype.clone(),
}
}
}
impl Rc for ResourceHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapResourceHandler for ResourceHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
self.object = object;
}
}
@@ -0,0 +1,86 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
use super::resource_handler::ResourceHandlerImpl;
use crate::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
pub(crate) struct SchemeHandlerFactoryImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> SchemeHandlerFactoryImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
object: std::ptr::null_mut(),
event_handler,
}
}
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
let mut scheme_options = 0;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
registrar.add_custom_scheme(Some(&CefString::from(RESOURCE_SCHEME)), scheme_options);
}
}
}
impl<H: CefEventHandler> ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
if let Some(scheme_name) = scheme_name {
if scheme_name.to_string() != RESOURCE_SCHEME {
return None;
}
if let Some(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url.strip_prefix(&format!("{RESOURCE_SCHEME}://")).unwrap();
let domain = path.split('/').next().unwrap_or("");
let path = path.strip_prefix(domain).unwrap_or("");
let path = path.trim_start_matches('/');
return match domain {
RESOURCE_DOMAIN => {
let resource = self.event_handler.load_resource(path.to_string().into());
Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)))
}
_ => None,
};
}
return None;
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
impl<H: CefEventHandler> Clone for SchemeHandlerFactoryImpl<H> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
}
}
impl<H: CefEventHandler> Rc for SchemeHandlerFactoryImpl<H> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<H: CefEventHandler> WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
-223
View File
@@ -1,223 +0,0 @@
use std::cell::RefCell;
use std::ffi::c_int;
use std::ops::DerefMut;
use std::slice::Iter;
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_resource_handler_t, _cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{
Browser, Callback, CefString, Frame, ImplRequest, ImplResourceHandler, ImplResponse, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, ResourceReadCallback, Response,
SchemeRegistrar, WrapResourceHandler, WrapSchemeHandlerFactory,
};
use include_dir::{Dir, include_dir};
use super::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
pub(crate) struct GraphiteSchemeHandlerFactory {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
}
impl GraphiteSchemeHandlerFactory {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
let mut scheme_options = 0;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
registrar.add_custom_scheme(Some(&CefString::from(GRAPHITE_SCHEME)), scheme_options);
}
}
}
impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
if let Some(scheme_name) = scheme_name {
if scheme_name.to_string() != GRAPHITE_SCHEME {
return None;
}
if let Some(request) = request {
let url = CefString::from(&request.url()).to_string();
let path = url.strip_prefix(&format!("{GRAPHITE_SCHEME}://")).unwrap();
let domain = path.split('/').next().unwrap_or("");
let path = path.strip_prefix(domain).unwrap_or("");
let path = path.trim_start_matches('/');
return match domain {
FRONTEND_DOMAIN => {
if path.is_empty() {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new("index.html")))
} else {
Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new(path)))
}
}
_ => None,
};
}
return None;
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
struct GraphiteFrontendResourceHandler<'a> {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
data: Option<RefCell<Iter<'a, u8>>>,
mimetype: Option<String>,
}
impl<'a> GraphiteFrontendResourceHandler<'a> {
pub fn new(path: &str) -> Self {
let file = FRONTEND.get_file(path);
let data = if let Some(file) = file {
Some(RefCell::new(file.contents().iter()))
} else {
tracing::error!("Failed to find asset at path: {}", path);
None
};
let mimetype = if let Some(file) = file {
let ext = file.path().extension().and_then(|s| s.to_str()).unwrap_or("");
// We know what file types will be in the assets this should be fine
match ext {
"html" => Some("text/html".to_string()),
"css" => Some("text/css".to_string()),
"txt" => Some("text/plain".to_string()),
"wasm" => Some("application/wasm".to_string()),
"js" => Some("application/javascript".to_string()),
"png" => Some("image/png".to_string()),
"jpg" | "jpeg" => Some("image/jpeg".to_string()),
"svg" => Some("image/svg+xml".to_string()),
"xml" => Some("application/xml".to_string()),
"json" => Some("application/json".to_string()),
"ico" => Some("image/x-icon".to_string()),
"woff" => Some("font/woff".to_string()),
"woff2" => Some("font/woff2".to_string()),
"ttf" => Some("font/ttf".to_string()),
"otf" => Some("font/otf".to_string()),
"webmanifest" => Some("application/manifest+json".to_string()),
"graphite" => Some("application/graphite+json".to_string()),
_ => None,
}
} else {
None
};
Self {
object: std::ptr::null_mut(),
data,
mimetype,
}
}
}
impl<'a> ImplResourceHandler for GraphiteFrontendResourceHandler<'a> {
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
if let Some(handle_request) = handle_request {
*handle_request = 1;
}
1
}
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
if let Some(response_length) = response_length {
*response_length = -1; // Indicating that the length is unknown
}
if let Some(response) = response {
if self.data.is_some() {
if let Some(mimetype) = &self.mimetype {
let cef_mime = CefString::from(mimetype.as_str());
response.set_mime_type(Some(&cef_mime));
} else {
response.set_mime_type(None);
}
response.set_status(200);
} else {
response.set_status(404);
response.set_mime_type(Some(&CefString::from("text/plain")));
}
}
}
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
let mut read = 0;
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
if let Some(data) = &self.data {
let mut data = data.borrow_mut();
for (out, &data) in out.iter_mut().zip(data.deref_mut()) {
*out = data;
read += 1;
}
}
if let Some(bytes_read) = bytes_read {
*bytes_read = read;
}
if read > 0 {
1 // Indicating that data was read
} else {
0 // Indicating no data was read
}
}
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}
impl WrapSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
impl<'a> WrapResourceHandler for GraphiteFrontendResourceHandler<'a> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
self.object = object;
}
}
impl Clone for GraphiteSchemeHandlerFactory {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl<'a> Clone for GraphiteFrontendResourceHandler<'a> {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
data: self.data.clone(),
mimetype: self.mimetype.clone(),
}
}
}
impl Rc for GraphiteSchemeHandlerFactory {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<'a> Rc for GraphiteFrontendResourceHandler<'a> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
use std::process::exit; use std::process::exit;
use std::time::Instant; use std::time::Instant;
use cef::CefHandler;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop; use winit::event_loop::EventLoop;
@@ -30,7 +31,7 @@ pub(crate) enum CustomEvent {
fn main() { fn main() {
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
let cef_context_builder = cef::CefContextBuilder::new(); let cef_context_builder = cef::CefContextBuilder::<CefHandler>::new();
if cef_context_builder.is_sub_process() { if cef_context_builder.is_sub_process() {
// We are in a CEF subprocess // We are in a CEF subprocess
-1
View File
@@ -16,7 +16,6 @@ gpu = ["graphite-editor/gpu"]
# Local dependencies # Local dependencies
graphite-editor = { path = "../../editor", features = [ graphite-editor = { path = "../../editor", features = [
"gpu", "gpu",
"ron",
"vello", "vello",
] } ] }
graphene-std = { workspace = true } graphene-std = { workspace = true }
+1 -4
View File
@@ -16,7 +16,6 @@ wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"] gpu = ["interpreted-executor/gpu", "wgpu-executor"]
resvg = ["graphene-std/resvg"] resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"] vello = ["graphene-std/vello", "resvg"]
ron = []
[dependencies] [dependencies]
# Local dependencies # Local dependencies
@@ -45,9 +44,7 @@ once_cell = { workspace = true }
web-sys = { workspace = true } web-sys = { workspace = true }
vello = { workspace = true } vello = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
spin = { workspace = true }
# Required dependencies
spin = "0.9.8"
# Optional local dependencies # Optional local dependencies
wgpu-executor = { workspace = true, optional = true } wgpu-executor = { workspace = true, optional = true }
@@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, La
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData; use crate::messages::tool::utility_types::HintData;
use glam::IVec2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
@@ -124,12 +125,19 @@ pub enum FrontendMessage {
document_id: DocumentId, document_id: DocumentId,
}, },
UpdateImportsExports { UpdateImportsExports {
imports: Vec<(FrontendGraphOutput, i32, i32)>, /// If the primary import is not visible, then it is None.
exports: Vec<(FrontendGraphInput, i32, i32)>, imports: Vec<Option<FrontendGraphOutput>>,
#[serde(rename = "addImport")] /// If the primary export is not visible, then it is None.
add_import: Option<(i32, i32)>, exports: Vec<Option<FrontendGraphInput>>,
#[serde(rename = "addExport")] /// The primary import location.
add_export: Option<(i32, i32)>, #[serde(rename = "importPosition")]
import_position: IVec2,
/// The primary export location.
#[serde(rename = "exportPosition")]
export_position: IVec2,
/// The document network does not have an add import or export button.
#[serde(rename = "addImportExport")]
add_import_export: bool,
}, },
UpdateInSelectedNetwork { UpdateInSelectedNetwork {
#[serde(rename = "inSelectedNetwork")] #[serde(rename = "inSelectedNetwork")]
@@ -17,7 +17,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType; use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::portfolio::utility_types::PersistentData;
@@ -2766,7 +2766,7 @@ impl DocumentMessageHandler {
}); });
} }
pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) { pub fn update_layers_panel_bottom_bar_widgets(&mut self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open { if !layers_panel_open {
return; return;
} }
@@ -2776,6 +2776,7 @@ impl DocumentMessageHandler {
let selected_layer = selected_layers.next(); let selected_layer = selected_layers.next();
let has_selection = selected_layer.is_some(); let has_selection = selected_layer.is_some();
let has_multiple_selection = selected_layers.next().is_some(); let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let widgets = vec![ let widgets = vec![
PopoverButton::new() PopoverButton::new()
@@ -2789,7 +2790,7 @@ impl DocumentMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = self.network_interface.output_type(&node_id, 0, &self.selection_network_path); let (output_type, _) = self.network_interface.output_type(&OutputConnector::node(node_id, 0), &self.selection_network_path);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -382,7 +382,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![ inputs: vec![
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0), NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
], ],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default() ..Default::default()
@@ -771,6 +770,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![ exports: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(0), 0),
NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(2), 0),
@@ -825,8 +825,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
persistent_node_metadata: DocumentNodePersistentMetadata { persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Image", "TODO").into()], input_metadata: vec![("Image", "TODO").into()],
output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
has_primary_output: false,
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
@@ -883,7 +882,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)], inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
@@ -905,13 +904,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}), }),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)], inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
..Default::default() ..Default::default()
}, },
persistent_node_metadata: DocumentNodePersistentMetadata { persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Vec2", "TODO").into()], input_metadata: vec![("Vec2", "TODO").into()],
output_names: vec!["X".to_string(), "Y".to_string()], output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
has_primary_output: false,
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
@@ -77,7 +77,6 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
}) })
.collect(), .collect(),
output_names: vec![output_type.to_string()], output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false, locked: false,
..Default::default() ..Default::default()
}, },
@@ -18,7 +18,11 @@ pub enum NodeGraphMessage {
}, },
AddPathNode, AddPathNode,
AddImport, AddImport,
AddPrimaryImport,
AddSecondaryImport,
AddExport, AddExport,
AddPrimaryExport,
AddSecondaryExport,
Init, Init,
SelectedNodesUpdated, SelectedNodesUpdated,
Copy, Copy,
@@ -63,6 +67,12 @@ pub enum NodeGraphMessage {
set_to_exposed: bool, set_to_exposed: bool,
start_transaction: bool, start_transaction: bool,
}, },
ExposeEncapsulatingPrimaryInput {
exposed: bool,
},
ExposePrimaryExport {
exposed: bool,
},
InsertNode { InsertNode {
node_id: NodeId, node_id: NodeId,
node_template: NodeTemplate, node_template: NodeTemplate,
@@ -1,4 +1,4 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode}; use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode};
use super::{document_node_definitions, node_properties}; use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE; use crate::consts::GRID_SIZE;
use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::macros::action_keys;
@@ -127,7 +127,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
} }
NodeGraphMessage::AddPathNode => { NodeGraphMessage::AddPathNode => {
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) { if let Some(layer) = make_path_editable_is_allowed(network_interface) {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer }); responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
responses.add(EventMessage::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
@@ -136,10 +136,44 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path); network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
NodeGraphMessage::AddPrimaryImport => {
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
} else {
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: true });
}
}
NodeGraphMessage::AddSecondaryImport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false });
}
// Add the secondary import
responses.add(NodeGraphMessage::AddImport);
}
NodeGraphMessage::AddExport => { NodeGraphMessage::AddExport => {
network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path); network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
NodeGraphMessage::AddPrimaryExport => {
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
} else {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: true });
}
}
NodeGraphMessage::AddSecondaryExport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false });
}
// Add the secondary export
responses.add(NodeGraphMessage::AddExport);
}
NodeGraphMessage::Init => { NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: EventMessage::SelectionChanged, on: EventMessage::SelectionChanged,
@@ -401,6 +435,77 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::ZoomCanvasTo100Percent); responses.add(DocumentMessage::ZoomCanvasTo100Percent);
} }
} }
NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed } => {
let Some((node_id, network_path)) = breadcrumb_network_path.split_last() else {
return;
};
let encapsulating_connector = InputConnector::node(*node_id, 0);
if !exposed {
network_interface.disconnect_input(&encapsulating_connector, network_path);
}
let Some(mut input) = network_interface.input_from_connector(&encapsulating_connector, network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&encapsulating_connector, input, network_path);
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
// Disconnect all connections in the encapsulating network
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
}
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::ExposePrimaryExport { exposed } => {
let export_connector: InputConnector = InputConnector::Export(0);
if !exposed {
network_interface.disconnect_input(&export_connector, breadcrumb_network_path);
}
let Some(mut input) = network_interface.input_from_connector(&export_connector, breadcrumb_network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&export_connector, input, breadcrumb_network_path);
// Disconnect all connections in the encapsulating network
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, encapsulating_path);
}
}
responses.add(NodeGraphMessage::UpdateImportsExports);
}
NodeGraphMessage::InsertNode { node_id, node_template } => { NodeGraphMessage::InsertNode { node_id, node_template } => {
network_interface.insert_node(node_id, node_template, selection_network_path); network_interface.insert_node(node_id, node_template, selection_network_path);
} }
@@ -706,21 +811,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
if modify_import_export.add_import_export.clicked_input_port_from_point(node_graph_point).is_some() { if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddExport); if remove_import_index == 0 {
return; responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false })
} else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() { } else {
responses.add(DocumentMessage::AddTransaction); responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
responses.add(NodeGraphMessage::AddImport); }
return;
} else if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
return; return;
} else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) { } else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index }); if remove_export_index == 0 {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false })
} else {
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index });
}
return; return;
} else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) { } else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@@ -756,10 +861,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
// Alt-click sets the clicked node as previewed // Alt-click sets the clicked node as previewed
if alt_click { if alt_click && let Some(clicked_node) = clicked_id {
if let Some(clicked_node) = clicked_id { self.preview_on_mouse_up = Some(clicked_node);
self.preview_on_mouse_up = Some(clicked_node);
}
} }
// Begin moving an existing wire // Begin moving an existing wire
@@ -785,14 +888,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.initial_disconnecting = false; self.initial_disconnecting = false;
self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path); self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path);
if let Some((output_type, source)) = clicked_output let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path);
.node_id() self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source);
.map(|node_id| network_interface.output_type(&node_id, clicked_output.index(), breadcrumb_network_path))
{
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type, &source);
} else {
self.wire_in_progress_type = FrontendGraphDataType::General;
}
self.update_node_graph_hints(responses); self.update_node_graph_hints(responses);
return; return;
@@ -1023,7 +1120,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
(position > point.y).then_some(*index) (position > point.y).then_some(*index)
}) })
.unwrap_or(modify_import_export.reorder_imports_exports.output_ports().count()), .filter(|end_index| *end_index > 0) // An import cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.output_ports().count() + 1),
); );
responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index }); responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index });
} else if self.reordering_export.is_some() { } else if self.reordering_export.is_some() {
@@ -1043,7 +1141,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
(position > point.y).then_some(*index) (position > point.y).then_some(*index)
}) })
.unwrap_or(modify_import_export.reorder_imports_exports.input_ports().count()), .filter(|end_index| *end_index > 0) // An export cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.input_ports().count() + 1),
); );
responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index }); responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index });
} }
@@ -1098,27 +1197,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} else if output_connector.is_some() && input_connector.is_none() && !self.initial_disconnecting { } else if !self.initial_disconnecting
&& input_connector.is_none()
&& let Some(output_connector) = output_connector
{
// If the add node menu is already open, we don't want to open it again // If the add node menu is already open, we don't want to open it again
if self.context_menu.is_some() { if self.context_menu.is_some() {
return; return;
} }
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&output_connector, selection_network_path);
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
warn!("No network_metadata"); warn!("No network_metadata");
return; return;
}; };
// Get the compatible type from the output connector
let compatible_type = output_connector.and_then(|output_connector| {
output_connector.node_id().and_then(|node_id| {
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&node_id, output_connector.index(), selection_network_path);
match type_source { let compatible_type = match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None, TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())), _ => Some(format!("type:{}", output_type.nested_type())),
} };
})
});
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. }; let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. }; let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. };
let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x; let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x;
@@ -1442,11 +1541,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::RemoveImport { import_index: usize } => { NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path); network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
NodeGraphMessage::RemoveExport { export_index: usize } => { NodeGraphMessage::RemoveExport { export_index: usize } => {
network_interface.remove_export(usize, selection_network_path); network_interface.remove_export(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
@@ -1821,7 +1922,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let shift = ipp.keyboard.get(Key::Shift as usize); let shift = ipp.keyboard.get(Key::Shift as usize);
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
log::error!("Could not get selected nodes in PointerMove"); log::error!("Could not get selected nodes in UpdateBoxSelection");
return; return;
}; };
let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>(); let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>();
@@ -1850,29 +1951,25 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
} }
NodeGraphMessage::UpdateImportsExports => { NodeGraphMessage::UpdateImportsExports => {
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); let imports = network_interface.frontend_imports(breadcrumb_network_path);
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); let exports = network_interface.frontend_exports(breadcrumb_network_path);
let add_import = network_interface
.frontend_import_export_modify( let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else {
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::<Vec<_>>(), log::error!("Could not get import export positions");
breadcrumb_network_path, return;
) };
.into_iter()
.next(); // Do not show the add import or add export button in the document network;
let add_export = network_interface let add_import_export = !breadcrumb_network_path.is_empty();
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.input_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
responses.add(NodeGraphMessage::UpdateVisibleNodes); responses.add(NodeGraphMessage::UpdateVisibleNodes);
responses.add(NodeGraphMessage::SendWires); responses.add(NodeGraphMessage::SendWires);
responses.add(FrontendMessage::UpdateImportsExports { responses.add(FrontendMessage::UpdateImportsExports {
imports, imports,
exports, exports,
add_import, import_position,
add_export, export_position,
add_import_export,
}); });
} }
@@ -1990,11 +2087,6 @@ impl NodeGraphMessageHandler {
return; return;
}; };
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else {
warn!("No selected nodes in update_selection_action_buttons"); warn!("No selected nodes in update_selection_action_buttons");
return; return;
@@ -2008,6 +2100,7 @@ impl NodeGraphMessageHandler {
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata()); let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let selected_layer = selected_layers.next(); let selected_layer = selected_layers.next();
let has_multiple_selection = selected_layers.next().is_some(); let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let mut widgets = vec![ let mut widgets = vec![
PopoverButton::new() PopoverButton::new()
@@ -2020,7 +2113,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]); let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -2106,6 +2199,11 @@ impl NodeGraphMessageHandler {
let mut selection = selected_nodes.selected_nodes(); let mut selection = selected_nodes.selected_nodes();
let (selection, no_other_selections) = (selection.next(), selection.count() == 0); let (selection, no_other_selections) = (selection.next(), selection.count() == 0);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) { let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) {
network.exports.iter().find_map(|export| { network.exports.iter().find_map(|export| {
let NodeInput::Node { node_id, .. } = export else { return None }; let NodeInput::Node { node_id, .. } = export else { return None };
@@ -2338,7 +2436,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]); let (output_type, _) = context.network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -2426,110 +2524,48 @@ impl NodeGraphMessageHandler {
} }
fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> { fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> {
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path).cloned() else {
return Vec::new();
};
let mut can_be_layer_lookup = HashSet::new();
let mut position_lookup = HashMap::new();
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else { let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes"); log::error!("Could not get nested network when collecting nodes");
return Vec::new(); return Vec::new();
}; };
for node_id in network.nodes.keys().cloned().collect::<Vec<_>>() {
if network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path) {
can_be_layer_lookup.insert(node_id);
}
if let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) {
position_lookup.insert(node_id, position);
} else {
log::error!("Could not get position for node {node_id}");
}
}
let mut frontend_inputs_lookup = frontend_inputs_lookup(breadcrumb_network_path, network_interface);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
};
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
log::error!("Could not get network_metadata when collecting nodes");
return Vec::new();
};
let mut nodes = Vec::new(); let mut nodes = Vec::new();
for (&node_id, node) in &network.nodes { for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
let node_id_path = [breadcrumb_network_path, (&[node_id])].concat(); let node_id_path = [breadcrumb_network_path, &[node_id]].concat();
let inputs = frontend_inputs_lookup.remove(&node_id).unwrap_or_default(); let primary_input_connector = InputConnector::node(node_id, 0);
let mut inputs = inputs.into_iter().map(|input| { let primary_input = if network_interface
input.map(|input| FrontendGraphInput { .input_from_connector(&primary_input_connector, breadcrumb_network_path)
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source), .is_some_and(|input| input.is_exposed())
resolved_type: format!("{:?}", &input.ty), {
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(), network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path)
name: input.input_name,
description: input.input_description,
connected_to: input.output_connector,
})
});
let primary_input = inputs.next().flatten();
let exposed_inputs = inputs.flatten().collect();
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let frontend_data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let connected_to = outward_wires.get(&OutputConnector::node(node_id, 0)).cloned().unwrap_or_default();
let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
Some(FrontendGraphOutput {
data_type: frontend_data_type,
name: "Output 1".to_string(),
description: String::new(),
resolved_type: format!("{output_type:?}"),
connected_to,
})
} else { } else {
None None
}; };
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path))
.collect();
let mut exposed_outputs = Vec::new(); let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) {
if output_index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
continue;
}
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else { let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
log::error!("Could not get node_metadata when getting output for {node_id}"); .filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
continue; .collect();
}; let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) {
let output_name = node_metadata (
.persistent_metadata network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path),
.output_names network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path),
.get(output_index) )
.cloned() } else {
.filter(|output_name| !output_name.is_empty()) (false, false)
.unwrap_or_else(|| output_type.nested_type().to_string());
let connected_to = outward_wires.get(&OutputConnector::node(node_id, output_index)).cloned().unwrap_or_default();
exposed_outputs.push(FrontendGraphOutput {
data_type,
name: output_name,
description: String::new(),
resolved_type: format!("{output_type:?}"),
connected_to,
});
}
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
}; };
let is_export = network.exports.first().is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_export = network_interface
.input_from_connector(&InputConnector::Export(0), breadcrumb_network_path)
.is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id); let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id);
let Some(position) = position_lookup.get(&node_id).map(|pos| (pos.x, pos.y)) else { let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else {
log::error!("Could not get position for node: {node_id}"); log::error!("Could not get position for node: {node_id}");
continue; continue;
}; };
@@ -2555,19 +2591,20 @@ impl NodeGraphMessageHandler {
is_layer: network_interface is_layer: network_interface
.node_metadata(&node_id, breadcrumb_network_path) .node_metadata(&node_id, breadcrumb_network_path)
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
can_be_layer: can_be_layer_lookup.contains(&node_id), can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path),
reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(),
display_name: network_interface.display_name(&node_id, breadcrumb_network_path), display_name: network_interface.display_name(&node_id, breadcrumb_network_path),
primary_input, primary_input,
exposed_inputs, exposed_inputs,
primary_output, primary_output,
exposed_outputs, exposed_outputs,
primary_output_connected_to_layer,
primary_input_connected_to_layer,
position, position,
previewed, previewed,
visible: node.visible, visible,
locked, locked,
errors, errors,
ui_only: false,
}); });
} }
@@ -2717,73 +2754,6 @@ impl NodeGraphMessageHandler {
} }
} }
#[derive(Default)]
struct InputLookup {
input_name: String,
input_description: String,
ty: Type,
type_source: TypeSource,
valid_types: Vec<Type>,
output_connector: Option<OutputConnector>,
}
type FrontendInputsLookup = HashMap<NodeId, Vec<Option<InputLookup>>>;
/// Create a lookup hashmap that can be used to create the frontend inputs. This is needed because `input_type` requires a mutable `network_interface`.
fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: &mut NodeNetworkInterface) -> FrontendInputsLookup {
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
return Default::default();
};
let mut frontend_inputs_lookup = HashMap::new();
for (node_id, index, output_connector, is_exposed) in network
.nodes
.iter()
.flat_map(|(node_id, node)| {
node.inputs
.iter()
.enumerate()
.map(|(index, input)| (*node_id, index, OutputConnector::from_input(input), input.is_exposed()))
})
.collect::<Vec<_>>()
{
// Skip not exposed inputs (they still get an entry to help with finding the primary input)
let lookup = if !is_exposed {
None
} else {
// Get the name from the metadata here (since it also requires a reference to the `network_interface`)
let (input_name, input_description) = network_interface.displayed_input_name_and_description(&node_id, index, breadcrumb_network_path);
Some(InputLookup {
input_name,
input_description,
output_connector,
..Default::default()
})
};
frontend_inputs_lookup.entry(node_id).or_insert_with(Vec::new).push(lookup);
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path);
value.ty = ty;
value.type_source = type_source;
}
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
value.valid_types = network_interface.valid_input_types(&InputConnector::node(node_id, index), breadcrumb_network_path);
}
}
frontend_inputs_lookup
}
impl Default for NodeGraphMessageHandler { impl Default for NodeGraphMessageHandler {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -173,6 +173,8 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(), Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false), Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets), Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default()),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default()),
// ========================== // ==========================
// PRIMITIVE COLLECTION TYPES // PRIMITIVE COLLECTION TYPES
// ========================== // ==========================
@@ -926,6 +928,22 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input // Add the color input
match &**tagged_value { match &**tagged_value {
TaggedValue::ColorNotInTable(color) => widgets.push(
color_button
.value(FillChoice::Solid(*color))
.allow_none(false)
.on_update(update_value(|input: &ColorInput| TaggedValue::ColorNotInTable(input.value.as_solid().unwrap()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColorNotInTable(color) => widgets.push(
color_button
.value(color.map_or(FillChoice::None, FillChoice::Solid))
.allow_none(true)
.on_update(update_value(|input: &ColorInput| TaggedValue::OptionalColorNotInTable(input.value.as_solid()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::Color(color_table) => widgets.push( TaggedValue::Color(color_table) => widgets.push(
color_button color_button
.value(match color_table.iter().next() { .value(match color_table.iter().next() {
@@ -965,7 +983,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
.on_commit(commit_value) .on_commit(commit_value)
.widget_holder(), .widget_holder(),
), ),
_ => {} x => warn!("Colour {x:?}"),
} }
LayoutGroup::Row { widgets } LayoutGroup::Row { widgets }
@@ -1,4 +1,5 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource}; use crate::messages::portfolio::document::utility_types::network_interface::TypeSource;
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;
@@ -60,7 +61,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "validTypes")] #[serde(rename = "validTypes")]
pub valid_types: Vec<String>, pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")] #[serde(rename = "connectedTo")]
pub connected_to: Option<OutputConnector>, /// Either "nothing", "import index {index}", or "{node name} output {output_index}".
pub connected_to: String,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -68,11 +70,13 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")] #[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType, pub data_type: FrontendGraphDataType,
pub name: String, pub name: String,
pub description: String,
#[serde(rename = "resolvedType")] #[serde(rename = "resolvedType")]
pub resolved_type: String, pub resolved_type: String,
pub description: String,
/// If connected to an export, it is "export index {index}".
/// If connected to a node, it is "{node name} input {input_index}".
#[serde(rename = "connectedTo")] #[serde(rename = "connectedTo")]
pub connected_to: Vec<InputConnector>, pub connected_to: Vec<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -93,13 +97,15 @@ pub struct FrontendNode {
pub primary_output: Option<FrontendGraphOutput>, pub primary_output: Option<FrontendGraphOutput>,
#[serde(rename = "exposedOutputs")] #[serde(rename = "exposedOutputs")]
pub exposed_outputs: Vec<FrontendGraphOutput>, pub exposed_outputs: Vec<FrontendGraphOutput>,
pub position: (i32, i32), #[serde(rename = "primaryOutputConnectedToLayer")]
pub primary_output_connected_to_layer: bool,
#[serde(rename = "primaryInputConnectedToLayer")]
pub primary_input_connected_to_layer: bool,
pub position: IVec2,
pub visible: bool, pub visible: bool,
pub locked: bool, pub locked: bool,
pub previewed: bool, pub previewed: bool,
pub errors: Option<String>, pub errors: Option<String>,
#[serde(rename = "uiOnly")]
pub ui_only: bool,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, InputPersistentMetadata, NodeNetworkMetadata, NodeTypePersistentMetadata};
use serde_json::Value;
use std::collections::HashMap;
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataInputNames {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_names: Vec<String>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataInputNames> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataInputNames) -> Self {
DocumentNodePersistentMetadata {
input_metadata: Vec::new(),
..old.into()
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataPropertiesRow {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_properties: Vec<PropertiesRow>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PropertiesRow {
pub input_data: HashMap<String, Value>,
pub widget_override: Option<String>,
#[serde(skip)]
pub input_name: String,
#[serde(skip)]
pub input_description: String,
}
impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataPropertiesRow) -> Self {
let mut input_metadata = Vec::new();
for properties_row in old.input_properties {
input_metadata.push(InputMetadata {
persistent_metadata: InputPersistentMetadata {
input_data: properties_row.input_data,
widget_override: properties_row.widget_override,
input_name: properties_row.input_name,
input_description: properties_row.input_description,
},
..Default::default()
})
}
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: Vec::new(),
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataHasPrimaryOutput {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_metadata: Vec<InputMetadata>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataHasPrimaryOutput> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataHasPrimaryOutput) -> Self {
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: old.input_metadata,
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
pub fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let value = Value::deserialize(deserializer)?;
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataHasPrimaryOutput>(value.clone()) {
return Ok(document.into());
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadata>(value.clone()) {
return Ok(document);
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataPropertiesRow>(value.clone()) {
return Ok(document.into());
};
match serde_json::from_value::<DocumentNodePersistentMetadataInputNames>(value.clone()) {
Ok(document) => Ok(document.into()),
Err(e) => Err(serde::de::Error::custom(e)),
}
}
@@ -230,6 +230,14 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
"graphene_math_nodes::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode",
], ],
}, },
NodeReplacement {
node: graphene_std::vector::cut_segments::IDENTIFIER,
aliases: &["graphene_core::vector::SplitSegmentsNode"],
},
NodeReplacement {
node: graphene_std::vector::cut_path::IDENTIFIER,
aliases: &["graphene_core::vector::SplitPathNode"],
},
NodeReplacement { NodeReplacement {
node: graphene_std::vector::vec_2_to_point::IDENTIFIER, node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
aliases: &["graphene_core::vector::PositionToPointNode"], aliases: &["graphene_core::vector::PositionToPointNode"],
@@ -109,7 +109,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let metadata = &document.network_interface.document_network_metadata().persistent_metadata; let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty()) (!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
}; };
self.menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some(); self.menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
} }
self.menu_bar_message_handler.process_message(message, responses, ()); self.menu_bar_message_handler.process_message(message, responses, ());
@@ -1144,7 +1144,7 @@ impl PortfolioMessageHandler {
} }
} }
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) { fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front { if to_front {
self.document_ids.push_front(document_id); self.document_ids.push_front(document_id);
} else { } else {
@@ -1,8 +1,8 @@
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager}; use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData}; use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT; use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::network_interface::{NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text}; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text};
@@ -568,25 +568,21 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec
(d1 * len1, d2 * len2) (d1 * len1, d2 * len2)
} }
pub fn make_path_editable_is_allowed(network_interface: &NodeNetworkInterface, metadata: &DocumentMetadata) -> Option<LayerNodeIdentifier> { pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterface) -> Option<LayerNodeIdentifier> {
// Must have exactly one layer selected // Must have exactly one layer selected
let selected_nodes = network_interface.selected_nodes(); let selected_nodes = network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(metadata); let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let first_layer = selected_layers.next()?; let first_layer = selected_layers.next()?;
if selected_layers.next().is_some() { if selected_layers.next().is_some() {
return None; return None;
} }
for _ in selected_layers {}
// Must be a layer of type Table<Vector> // Must be a layer of type Table<Vector>
let compatible_type = NodeGraphLayer::new(first_layer, network_interface) let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?;
.horizontal_layer_flow()
.nth(1) let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
.map(|node_id| { if output_type.nested_type() != concrete!(Table<Vector>).nested_type() {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]);
output_type.nested_type() == concrete!(Table<Vector>).nested_type()
})
.unwrap_or_default();
if !compatible_type {
return None; return None;
} }
@@ -4,6 +4,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::compass_rose::Axis; use crate::messages::tool::common_functionality::compass_rose::Axis;
use crate::messages::tool::common_functionality::measure;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping; use crate::messages::tool::common_functionality::snapping;
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
@@ -12,7 +13,7 @@ use crate::messages::tool::common_functionality::snapping::SnapManager;
use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::transformation_cage::*;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Artboard; use graphene_std::Artboard;
use graphene_std::renderer::Quad; use graphene_std::renderer::{Quad, Rect};
use graphene_std::table::Table; use graphene_std::table::Table;
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
@@ -241,6 +242,39 @@ impl Fsm for ArtboardToolFsmState {
tool_data.bounding_box_manager.take(); tool_data.bounding_box_manager.take();
} }
// Measure with Alt held down between selected artboard and hovered layers/artboards
// TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places
let alt_pressed = input.keyboard.get(Key::Alt as usize);
let quick_measurement_enabled = overlay_context.visibility_settings.quick_measurement();
let not_resizing = !matches!(state, ArtboardToolFsmState::ResizingBounds);
if quick_measurement_enabled && not_resizing && alt_pressed {
// Get the selected artboard bounds
let selected_artboard_bounds = tool_data.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer)).map(Rect::from_box);
// Find hovered artboard or regular layer
let hovered_artboard = ArtboardToolData::hovered_artboard(document, input);
let hovered_layer = document.click_xray(input).find(|&layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
// Get bounds for the hovered object (prioritize artboards)
let hovered_bounds = if let Some(artboard) = hovered_artboard {
document.metadata().bounding_box_document(artboard).map(Rect::from_box)
} else if let Some(layer) = hovered_layer {
document.metadata().bounding_box_document(layer).map(Rect::from_box)
} else {
None
};
// If both selected artboard and hovered object bounds exist, overlay measurement lines
if let (Some(selected_bounds), Some(hovered_bounds)) = (selected_artboard_bounds, hovered_bounds) {
// Don't measure if it's the same artboard
if selected_artboard_bounds != Some(hovered_bounds) {
let document_to_viewport = document.metadata().document_to_viewport;
measure::overlay(selected_bounds, hovered_bounds, document_to_viewport, document_to_viewport, &mut overlay_context);
}
}
}
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
@@ -3171,7 +3171,7 @@ impl Fsm for PathToolFsmState {
colinear, colinear,
}; };
tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some(); tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
tool_data.update_selection_status(shape_editor, document); tool_data.update_selection_status(shape_editor, document);
tool_data.update_merge_point_toggle(shape_editor, document, tool_action_data.preferences.vector_meshes); tool_data.update_merge_point_toggle(shape_editor, document, tool_action_data.preferences.vector_meshes);
+197 -178
View File
@@ -4,8 +4,7 @@
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { Node } from "@graphite/messages"; import type { FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
@@ -178,7 +177,7 @@
} }
function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
return value.resolvedType ? `Data Type:\n${value.resolvedType}` : `Data Type (Unresolved):\n${value.dataType}`; return `Data Type: ${value.resolvedType}`;
} }
function validTypesText(value: FrontendGraphInput): string { function validTypesText(value: FrontendGraphInput): string {
@@ -189,37 +188,11 @@
function outputConnectedToText(output: FrontendGraphOutput): string { function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) return "Connected to nothing"; if (output.connectedTo.length === 0) return "Connected to nothing";
return output.connectedTo return `Connected to:\n${output.connectedTo.join("\n")}`;
.map((inputConnector) => {
if ((inputConnector as Node).nodeId === undefined) return `Connected to export index ${inputConnector.index}`;
return `Connected to ${(inputConnector as Node).nodeId}, connector index ${inputConnector.index}`;
})
.join("\n");
} }
function inputConnectedToText(input: FrontendGraphInput): string { function inputConnectedToText(input: FrontendGraphInput): string {
if (input.connectedTo === undefined) return "Connected to nothing"; return `Connected to:\n${input.connectedTo}`;
if ((input.connectedTo as Node).nodeId === undefined) return `Connected to import index ${input.connectedTo.index}`;
return `Connected to ${(input.connectedTo as Node).nodeId}, connector index ${input.connectedTo.index}`;
}
function primaryOutputConnectedToLayer(node: FrontendNode): boolean {
let firstConnectedNode = Array.from($nodeGraph.nodes.values()).find((n) =>
node.primaryOutput?.connectedTo.some((connector) => {
if ((connector as Node).nodeId === undefined) return false;
if (connector.index !== 0n) return false;
return n.id === (connector as Node).nodeId || false;
}),
);
return firstConnectedNode?.isLayer || false;
}
function primaryInputConnectedToLayer(node: FrontendNode): boolean {
const connectedNode = Array.from($nodeGraph.nodes.values()).find((n) => {
if ((node.primaryInput?.connectedTo as Node) === undefined) return false;
return n.id === (node.primaryInput?.connectedTo as Node).nodeId;
});
return connectedNode?.isLayer || false;
} }
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
@@ -265,16 +238,12 @@
{ {
value: "node", value: "node",
label: "Node", label: "Node",
action: () => { action: () => toggleLayerDisplay(false, contextMenuData.nodeId),
toggleLayerDisplay(false, contextMenuData.nodeId);
},
}, },
{ {
value: "layer", value: "layer",
label: "Layer", label: "Layer",
action: () => { action: () => toggleLayerDisplay(true, contextMenuData.nodeId),
toggleLayerDisplay(true, contextMenuData.nodeId);
},
}, },
]} ]}
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
@@ -334,149 +303,181 @@
<!-- Import and Export connectors --> <!-- Import and Export connectors -->
<div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}> <div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
{#each $nodeGraph.imports as { outputMetadata, position }, index} {#if $nodeGraph.updateImportsExports}
<svg {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index}
xmlns="http://www.w3.org/2000/svg" {#if frontendOutput}
viewBox="0 0 8 8" <svg
class="connector" xmlns="http://www.w3.org/2000/svg"
data-connector="output" viewBox="0 0 8 8"
data-datatype={outputMetadata.dataType} class="connector"
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`} data-connector="output"
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`} data-datatype={frontendOutput.dataType}
style:--offset-left={position.x / 24} style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
style:--offset-top={position.y / 24} style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
> style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
<title>{`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`}</title> style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
{#if outputMetadata.connectedTo !== undefined} >
<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)" /> <title>{`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`}</title>
{:else} {#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-dim)" /> <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)" />
{/if} {:else}
</svg> <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-dim)" />
{/if}
</svg>
<div <div
class="edit-import-export import" on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerenter={() => (hoveringImportIndex = index)} on:pointerleave={() => (hoveringImportIndex = undefined)}
on:pointerleave={() => (hoveringImportIndex = undefined)} class="edit-import-export import"
style:--offset-left={position.x / 24} class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-top={position.y / 24} class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
> style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
{#if editingNameImportIndex == index} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
<input >
class="import-text-input" {#if editingNameImportIndex == index}
type="text" <input
style:width={importsToEdgeTextInputWidth()} class="import-text-input"
bind:this={inputElement} type="text"
bind:value={editingNameText} style:width={importsToEdgeTextInputWidth()}
on:blur={setEditingImportName} bind:this={inputElement}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)} bind:value={editingNameText}
/> on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
{:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, frontendOutput.name)}>
{frontendOutput.name}
</p>
{/if}
{#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
<IconButton
size={16}
icon={"Remove"}
class="remove-button-import"
data-index={index}
data-import-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{#if index > 0}
<div class="reorder-drag-grip" title="Reorder this export" />
{/if}
{/if}
</div>
{:else} {:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p> <div
class="plus"
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} />
</div>
{/if} {/if}
{#if hoveringImportIndex === index || editingNameImportIndex === index} {/each}
<IconButton
size={16} {#each $nodeGraph.updateImportsExports.exports as frontendInput, index}
icon={"Remove"} {#if frontendInput}
class="remove-button-import" <svg
data-index={index} xmlns="http://www.w3.org/2000/svg"
data-import-text-edge viewBox="0 0 8 8"
action={() => { class="connector"
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ data-connector="input"
}} data-datatype={frontendInput.dataType}
/> style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
<div class="reorder-drag-grip" title="Reorder this import"></div> style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
{/if} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
</div> style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
{/each} >
{#if $nodeGraph.reorderImportIndex !== undefined} <title>{`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`}</title>
{@const position = { {#if frontendInput.connectedTo !== "nothing"}
x: Number($nodeGraph.imports[0].position.x), <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)" />
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24, {:else}
}} <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-dim)" />
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} /> {/if}
{/if} </svg>
{#if $nodeGraph.addImport !== undefined} <div
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}> on:pointerenter={() => (hoveringExportIndex = index)}
<IconButton on:pointerleave={() => (hoveringExportIndex = undefined)}
size={24} class="edit-import-export export"
icon="Add" class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
action={() => { class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
}} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
/> >
</div> {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{/if} {#if index > 0}
{#each $nodeGraph.exports as { inputMetadata, position }, index} <div class="reorder-drag-grip" title="Reorder this export" />
<svg {/if}
xmlns="http://www.w3.org/2000/svg" <IconButton
viewBox="0 0 8 8" size={16}
class="connector" icon={"Remove"}
data-connector="input" class="remove-button-export"
data-datatype={inputMetadata.dataType} data-index={index}
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`} data-export-text-edge
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`} action={() => {
style:--offset-left={position.x / 24} /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
style:--offset-top={position.y / 24} }}
> />
<title>{`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`}</title> {/if}
{#if inputMetadata.connectedTo !== undefined} {#if editingNameExportIndex === index}
<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)" /> <input
type="text"
style:width={exportsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingExportName}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
/>
{:else}
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, frontendInput.name)}>
{frontendInput.name}
</p>
{/if}
</div>
{:else} {:else}
<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-dim)" /> <div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} />
</div>
{/if} {/if}
</svg> {/each}
<div
class="edit-import-export export" {#if $nodeGraph.updateImportsExports.addImportExport == true}
on:pointerenter={() => (hoveringExportIndex = index)} <div
on:pointerleave={() => (hoveringExportIndex = undefined)} class="plus"
style:--offset-left={position.x / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
style:--offset-top={position.y / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
> >
{#if hoveringExportIndex === index || editingNameExportIndex === index} <IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
<div class="reorder-drag-grip" title="Reorder this export"></div> </div>
<IconButton <div
size={16} class="plus"
icon={"Remove"} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
class="remove-button-export" style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
data-index={index} >
data-export-text-edge <IconButton size={24} icon={"Add"} action={() => editor.handle.addSecondaryExport()} />
action={() => { </div>
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ {/if}
}}
/> {#if $nodeGraph.reorderImportIndex !== undefined}
{/if} {@const position = {
{#if editingNameExportIndex === index} x: Number($nodeGraph.updateImportsExports.importPosition.x),
<input y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24,
type="text" }}
style:width={exportsToEdgeTextInputWidth()} <div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24} />
bind:this={inputElement} {/if}
bind:value={editingNameText}
on:blur={setEditingExportName} {#if $nodeGraph.reorderExportIndex !== undefined}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)} {@const position = {
/> x: Number($nodeGraph.updateImportsExports.exportPosition.x),
{:else} y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24,
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p> }}
{/if} <div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24} />
</div> {/if}
{/each}
{#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.exports[0].position.x),
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
<IconButton
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if} {/if}
</div> </div>
@@ -530,7 +531,7 @@
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title> <title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo.length > 0} {#if node.primaryOutput.connectedTo.length > 0}
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" /> <path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
{#if primaryOutputConnectedToLayer(node)} {#if node.primaryOutputConnectedToLayer}
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" /> <path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
{/if} {/if}
{:else} {:else}
@@ -551,9 +552,9 @@
{#if node.primaryInput} {#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title> <title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{/if} {/if}
{#if node.primaryInput?.connectedTo !== undefined} {#if node.primaryInput?.connectedTo !== "nothing"}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" /> <path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
{#if primaryInputConnectedToLayer(node)} {#if node.primaryInputConnectedToLayer}
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" /> <path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
{/if} {/if}
{:else} {:else}
@@ -937,6 +938,24 @@
margin-top: -5px; margin-top: -5px;
height: 24px; height: 24px;
&.separator-bottom::after,
&.separator-top::before {
content: "";
position: absolute;
background: var(--color-8-uppergray);
height: 1px;
left: -4px;
right: -4px;
}
&.separator-bottom::after {
bottom: -1px;
}
&.separator-top::before {
top: 0;
}
&.import { &.import {
right: calc(100% - var(--offset-left) * 24px); right: calc(100% - var(--offset-left) * 24px);
} }
+13 -64
View File
@@ -12,12 +12,6 @@ export class JsMessage {
} }
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] })); const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
const ImportsToVec2Array = Transform(({ obj: { imports } }: { obj: { imports: [FrontendGraphOutput, number, number][] } }) =>
imports.map(([outputMetadata, x, y]) => ({ outputMetadata, position: { x, y } })),
);
const ExportsToVec2Array = Transform(({ obj: { exports } }: { obj: { exports: [FrontendGraphInput, number, number][] } }) =>
exports.map(([inputMetadata, x, y]) => ({ inputMetadata, position: { x, y } })),
);
// const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) })); // const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
@@ -58,17 +52,17 @@ export class UpdateContextMenuInformation extends JsMessage {
} }
export class UpdateImportsExports extends JsMessage { export class UpdateImportsExports extends JsMessage {
@ImportsToVec2Array readonly imports!: (FrontendGraphOutput | undefined)[];
readonly imports!: { outputMetadata: FrontendGraphOutput; position: XY }[];
@ExportsToVec2Array readonly exports!: (FrontendGraphInput | undefined)[];
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[];
@TupleToVec2 @TupleToVec2
readonly addImport!: XY | undefined; readonly importPosition!: XY;
@TupleToVec2 @TupleToVec2
readonly addExport!: XY | undefined; readonly exportPosition!: XY;
readonly addImportExport!: boolean;
} }
export class UpdateInSelectedNetwork extends JsMessage { export class UpdateInSelectedNetwork extends JsMessage {
@@ -194,29 +188,6 @@ export type ContextMenuInformation = {
export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color"; export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color";
export class Node {
readonly index!: bigint;
// Omitted if this Node is an Import or Export to/from the node network
readonly nodeId?: bigint;
}
const CreateOutputConnectorOptional = Transform(({ obj }) => {
if (obj.connectedTo == undefined) {
return undefined;
}
if (obj.connectedTo?.export !== undefined) {
return { index: obj.connectedTo?.export };
} else if (obj.connectedTo?.import !== undefined) {
return { index: obj.connectedTo?.import };
} else {
if (obj.connectedTo?.node.inputIndex !== undefined) {
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.inputIndex };
} else {
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.outputIndex };
}
}
});
export class FrontendGraphInput { export class FrontendGraphInput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
@@ -228,28 +199,9 @@ export class FrontendGraphInput {
readonly validTypes!: string[]; readonly validTypes!: string[];
@CreateOutputConnectorOptional readonly connectedTo!: string;
connectedTo!: Node | undefined;
} }
const CreateInputConnectorArray = Transform(({ obj }) => {
const newInputConnectors: Node[] = [];
obj.connectedTo.forEach((connector: any) => {
if (connector.export !== undefined) {
newInputConnectors.push({ index: connector.export });
} else if (connector.import !== undefined) {
newInputConnectors.push({ index: connector.import });
} else {
if (connector.node.inputIndex !== undefined) {
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.inputIndex });
} else {
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.outputIndex });
}
}
});
return newInputConnectors;
});
export class FrontendGraphOutput { export class FrontendGraphOutput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
@@ -259,8 +211,7 @@ export class FrontendGraphOutput {
readonly resolvedType!: string; readonly resolvedType!: string;
@CreateInputConnectorArray readonly connectedTo!: string[];
connectedTo!: Node[];
} }
export class FrontendNode { export class FrontendNode {
@@ -274,20 +225,20 @@ export class FrontendNode {
readonly displayName!: string; readonly displayName!: string;
@Type(() => FrontendGraphInput)
readonly primaryInput!: FrontendGraphInput | undefined; readonly primaryInput!: FrontendGraphInput | undefined;
@Type(() => FrontendGraphInput)
readonly exposedInputs!: FrontendGraphInput[]; readonly exposedInputs!: FrontendGraphInput[];
@Type(() => FrontendGraphOutput)
readonly primaryOutput!: FrontendGraphOutput | undefined; readonly primaryOutput!: FrontendGraphOutput | undefined;
@Type(() => FrontendGraphOutput)
readonly exposedOutputs!: FrontendGraphOutput[]; readonly exposedOutputs!: FrontendGraphOutput[];
readonly primaryInputConnectedToLayer!: boolean;
readonly primaryOutputConnectedToLayer!: boolean;
@TupleToVec2 @TupleToVec2
readonly position!: XY | undefined; readonly position!: XY;
// TODO: Store field for the width of the left node chain // TODO: Store field for the width of the left node chain
@@ -298,8 +249,6 @@ export class FrontendNode {
readonly unlocked!: boolean; readonly unlocked!: boolean;
readonly errors!: string | undefined; readonly errors!: string | undefined;
readonly uiOnly!: boolean;
} }
export class FrontendNodeType { export class FrontendNodeType {
+2 -9
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 type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/messages";
import { import {
type Box, type Box,
type FrontendClickTargets, type FrontendClickTargets,
@@ -37,10 +36,7 @@ export function createNodeGraphState(editor: Editor) {
layerWidths: new Map<bigint, number>(), layerWidths: new Map<bigint, number>(),
chainWidths: new Map<bigint, number>(), chainWidths: new Map<bigint, number>(),
hasLeftInputWire: new Map<bigint, boolean>(), hasLeftInputWire: new Map<bigint, boolean>(),
imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[], updateImportsExports: undefined as UpdateImportsExports | undefined,
exports: [] as { inputMetadata: FrontendGraphInput; position: { x: number; y: number } }[],
addImport: undefined as { x: number; y: number } | undefined,
addExport: undefined as { x: number; y: number } | undefined,
nodes: new Map<bigint, FrontendNode>(), nodes: new Map<bigint, FrontendNode>(),
visibleNodes: new Set<bigint>(), visibleNodes: new Set<bigint>(),
/// The index is the exposed input index. The exports have a first key value of u32::MAX. /// The index is the exposed input index. The exports have a first key value of u32::MAX.
@@ -96,10 +92,7 @@ export function createNodeGraphState(editor: Editor) {
}); });
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => { editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => {
update((state) => { update((state) => {
state.imports = updateImportsExports.imports; state.updateImportsExports = updateImportsExports;
state.exports = updateImportsExports.exports;
state.addImport = updateImportsExports.addImport;
state.addExport = updateImportsExports.addExport;
return state; return state;
}); });
}); });
+24
View File
@@ -314,6 +314,30 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
#[wasm_bindgen(js_name = addPrimaryImport)]
pub fn add_primary_import(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddPrimaryImport);
}
#[wasm_bindgen(js_name = addSecondaryImport)]
pub fn add_secondary_import(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddSecondaryImport);
}
#[wasm_bindgen(js_name = addPrimaryExport)]
pub fn add_primary_export(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddPrimaryExport);
}
#[wasm_bindgen(js_name = addSecondaryExport)]
pub fn add_secondary_export(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddSecondaryExport);
}
/// Toggles minimizing or restoring down the application window /// Toggles minimizing or restoring down the application window
#[wasm_bindgen(js_name = appWindowMaximize)] #[wasm_bindgen(js_name = appWindowMaximize)]
pub fn app_window_maximize(&self) { pub fn app_window_maximize(&self) {
+1 -1
View File
@@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
pest = "2.7" pest = "2.7"
pest_derive = "2.7.11" pest_derive = "2.7"
thiserror = "2.0" thiserror = "2.0"
lazy_static = "1.5" lazy_static = "1.5"
num-complex = "0.4" num-complex = "0.4"
+1 -1
View File
@@ -15,7 +15,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
/// The X or Y component of a vec2. /// The X or Y component of a vec2.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)] #[widget(Radio)]
pub enum XY { pub enum XY {
#[default] #[default]
X, X,
@@ -257,7 +257,7 @@ pub fn pathseg_normals_to_point(segment: PathSeg, point: Point) -> Vec<f64> {
5. * (x[3] * x[2] + y[3] * y[2]), 5. * (x[3] * x[2] + y[3] * y[2]),
3. * (x[3] * x[3] + y[3] * y[3]), 3. * (x[3] * x[3] + y[3] * y[3]),
]); ]);
poly.roots_between(0., 1., 1e-8) poly.roots_between(0., 1., 1e-8).to_vec()
} }
/// Find the `t`-value(s) such that the tangent(s) at `t` pass through the given point. /// Find the `t`-value(s) such that the tangent(s) at `t` pass through the given point.
+33 -4
View File
@@ -942,6 +942,35 @@ async fn solidify_stroke(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
.collect() .collect()
} }
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn separate_subpaths(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
content
.into_iter()
.flat_map(|row| {
let style = row.element.style.clone();
let transform = row.transform;
let alpha_blending = row.alpha_blending;
let source_node_id = row.source_node_id;
row.element
.stroke_bezpath_iter()
.map(move |bezpath| {
let mut vector = Vector::default();
vector.append_bezpath(bezpath);
vector.style = style.clone();
TableRow {
element: vector,
transform,
alpha_blending,
source_node_id,
}
})
.collect::<Vec<TableRow<Vector>>>()
})
.collect()
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: Table<I>) -> Table<Vector> async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: Table<I>) -> Table<Vector>
where where
@@ -1065,11 +1094,11 @@ async fn sample_polyline(
.collect() .collect()
} }
/// Splits a path at a given progress from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed). /// Cuts a path at a given progress from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed).
/// ///
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn split_path(_: impl Ctx, mut content: Table<Vector>, progress: Fraction, parameterized_distance: bool, reverse: bool) -> Table<Vector> { async fn cut_path(_: impl Ctx, mut content: Table<Vector>, progress: Fraction, parameterized_distance: bool, reverse: bool) -> Table<Vector> {
let euclidian = !parameterized_distance; let euclidian = !parameterized_distance;
let bezpaths = content let bezpaths = content
@@ -1108,9 +1137,9 @@ async fn split_path(_: impl Ctx, mut content: Table<Vector>, progress: Fraction,
content content
} }
/// Splits path segments into separate disconnected pieces where each is a distinct subpath. /// Cuts path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn split_segments(_: impl Ctx, mut content: Table<Vector>) -> Table<Vector> { async fn cut_segments(_: impl Ctx, mut content: Table<Vector>) -> Table<Vector> {
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
for row in content.iter_mut() { for row in content.iter_mut() {
let points_count = row.element.point_domain.ids().len(); let points_count = row.element.point_domain.ids().len();
@@ -171,6 +171,8 @@ tagged_value! {
Bool(bool), Bool(bool),
String(String), String(String),
OptionalF64(Option<f64>), OptionalF64(Option<f64>),
ColorNotInTable(Color),
OptionalColorNotInTable(Option<Color>),
// ======================== // ========================
// LISTS OF PRIMITIVE TYPES // LISTS OF PRIMITIVE TYPES
// ======================== // ========================
@@ -358,6 +360,8 @@ impl TaggedValue {
x if x == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?, x if x == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
x if x == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?, x if x == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
x if x == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?, x if x == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
x if x == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::ColorNotInTable(color))?,
x if x == TypeId::of::<Option<Color>>() => TaggedValue::ColorNotInTable(to_color(string)?),
x if x == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?, x if x == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
x if x == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, x if x == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => return None, _ => return None,
@@ -512,3 +516,8 @@ mod fake_hash {
} }
} }
} }
#[test]
fn can_construct_color() {
assert_eq!(TaggedValue::from_type(&concrete!(Color)).unwrap(), TaggedValue::ColorNotInTable(Color::default()));
}
+1 -3
View File
@@ -27,9 +27,7 @@ fern = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] } tokio = { workspace = true, features = ["rt-multi-thread"] }
clap = { workspace = true, features = ["cargo", "derive"] }
# Required dependencies
clap = { version = "4.5.31", features = ["cargo", "derive"] }
# Optional local dependencies # Optional local dependencies
wgpu-executor = { path = "../wgpu-executor", optional = true } wgpu-executor = { path = "../wgpu-executor", optional = true }
+1 -4
View File
@@ -307,7 +307,7 @@ fn black_and_white<T: Adjust<Color>>(
GradientStops, GradientStops,
)] )]
mut image: T, mut image: T,
#[default(Color::BLACK)] tint: Table<Color>, #[default(Color::BLACK)] tint: Color,
#[default(40.)] #[default(40.)]
#[range((-200., 300.))] #[range((-200., 300.))]
reds: PercentageF32, reds: PercentageF32,
@@ -327,9 +327,6 @@ fn black_and_white<T: Adjust<Color>>(
#[range((-200., 300.))] #[range((-200., 300.))]
magentas: PercentageF32, magentas: PercentageF32,
) -> T { ) -> T {
let tint: Option<Color> = tint.into();
let tint = tint.unwrap_or(Color::BLACK);
image.adjust(|color| { image.adjust(|color| {
let color = color.to_gamma_srgb(); let color = color.to_gamma_srgb();
+2 -11
View File
@@ -166,15 +166,12 @@ fn color_overlay<T: Adjust<Color>>(
GradientStops, GradientStops,
)] )]
mut image: T, mut image: T,
#[default(Color::BLACK)] color: Table<Color>, #[default(Color::BLACK)] color: Color,
blend_mode: BlendMode, blend_mode: BlendMode,
#[default(100.)] opacity: PercentageF32, #[default(100.)] opacity: PercentageF32,
) -> T { ) -> T {
let opacity = (opacity as f32 / 100.).clamp(0., 1.); let opacity = (opacity as f32 / 100.).clamp(0., 1.);
let color: Option<Color> = color.into();
let color = color.unwrap_or(Color::BLACK);
image.adjust(|pixel| { image.adjust(|pixel| {
let image = pixel.map_rgb(|channel| channel * (1. - opacity)); let image = pixel.map_rgb(|channel| channel * (1. - opacity));
@@ -206,13 +203,7 @@ mod test {
// 100% of the output should come from the multiplied value // 100% of the output should come from the multiplied value
let opacity = 100.; let opacity = 100.;
let result = super::color_overlay( let result = super::color_overlay((), Table::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity);
(),
Table::new_from_element(Raster::new_cpu(image.clone())),
Table::new_from_element(overlay_color),
BlendMode::Multiply,
opacity,
);
let result = result.iter().next().unwrap().element; let result = result.iter().next().unwrap().element;
// The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0)
+2 -6
View File
@@ -17,12 +17,8 @@ fn text<'i: 'n>(
#[unit(" px")] #[unit(" px")]
#[default(0.)] #[default(0.)]
character_spacing: f64, character_spacing: f64,
#[unit(" px")] #[unit(" px")] max_width: Option<f64>,
#[default(None)] #[unit(" px")] max_height: Option<f64>,
max_width: Option<f64>,
#[unit(" px")]
#[default(None)]
max_height: Option<f64>,
/// Faux italic. /// Faux italic.
#[unit("°")] #[unit("°")]
#[default(0.)] #[default(0.)]
+226 -141
View File
@@ -19,7 +19,7 @@ use graphene_core::transform::{Footprint, Transform};
use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::uuid::{NodeId, generate_uuid};
use graphene_core::vector::Vector; use graphene_core::vector::Vector;
use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::click_target::{ClickTarget, FreePoint};
use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode};
use graphene_core::{Artboard, Graphic}; use graphene_core::{Artboard, Graphic};
use kurbo::Affine; use kurbo::Affine;
use num_traits::Zero; use num_traits::Zero;
@@ -202,6 +202,12 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
}) + ")" }) + ")"
} }
fn max_scale(transform: DAffine2) -> f64 {
let sx = transform.x_axis.length_squared();
let sy = transform.y_axis.length_squared();
(sx + sy).sqrt()
}
pub fn to_transform(transform: DAffine2) -> usvg::Transform { pub fn to_transform(transform: DAffine2) -> usvg::Transform {
let cols = transform.to_cols_array(); let cols = transform.to_cols_array();
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
@@ -690,11 +696,36 @@ impl Render for Table<Vector> {
} else { } else {
MaskType::Mask MaskType::Mask
}; };
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered());
let can_use_paint_order = !(row.element.style.fill().is_none() || mask_type == MaskType::Clip); let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip);
let push_id = if can_draw_aligned_stroke && !can_use_paint_order { let needs_separate_fill = can_draw_aligned_stroke && !can_use_paint_order;
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
if needs_separate_fill && !wants_stroke_below {
render.leaf_tag("path", |attributes| {
attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let mut style = row.element.style.clone();
style.clear_stroke();
let fill_and_stroke = style.render(
&mut attributes.0.svg_defs,
element_transform,
applied_stroke_transform,
bounds_matrix,
transformed_bounds_matrix,
&render_params,
);
attributes.push_val(fill_and_stroke);
});
}
let push_id = needs_separate_fill.then_some({
let id = format!("alignment-{}", generate_uuid()); let id = format!("alignment-{}", generate_uuid());
let mut element = row.element.clone(); let mut element = row.element.clone();
@@ -708,13 +739,11 @@ impl Render for Table<Vector> {
source_node_id: None, source_node_id: None,
}); });
Some((id, mask_type, vector_row)) (id, mask_type, vector_row)
} else { });
None
};
render.leaf_tag("path", |attributes| { render.leaf_tag("path", |attributes| {
attributes.push("d", path); attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform); let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push("transform", matrix); attributes.push("transform", matrix);
@@ -724,16 +753,24 @@ impl Render for Table<Vector> {
if let Some((ref id, mask_type, ref vector_row)) = push_id { if let Some((ref id, mask_type, ref vector_row)) = push_id {
let mut svg = SvgRender::new(); let mut svg = SvgRender::new();
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = row.element.style.stroke().unwrap();
let weight = row.element.style.stroke().unwrap().weight * row.transform.matrix2.determinant(); let weight = stroke.effective_width() * max_scale(applied_stroke_transform);
let quad = Quad::from_box(transformed_bounds).inflate(weight); let quad = Quad::from_box(transformed_bounds).inflate(weight);
let (x, y) = quad.top_left().into(); let (x, y) = quad.top_left().into();
let (width, height) = (quad.bottom_right() - quad.top_left()).into(); let (width, height) = (quad.bottom_right() - quad.top_left()).into();
write!(defs, r##"{}"##, svg.svg_defs).unwrap(); write!(defs, r##"{}"##, svg.svg_defs).unwrap();
let rect = format!(r##"<rect x="{x}" y="{y}" width="{width}" height="{height}" fill="white" />"##); let rect = format!(r##"<rect x="{x}" y="{y}" width="{width}" height="{height}" fill="white" />"##);
match mask_type { match mask_type {
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(), MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
MaskType::Mask => write!(defs, r##"<mask id="{id}">{}{}</mask>"##, rect, svg.svg.to_svg_string()).unwrap(), MaskType::Mask => write!(
defs,
r##"<mask id="{id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="{x}" y="{y}" width="{width}" height="{height}">{}{}</mask>"##,
rect,
svg.svg.to_svg_string()
)
.unwrap(),
} }
} }
@@ -741,10 +778,12 @@ impl Render for Table<Vector> {
render_params.aligned_strokes = can_draw_aligned_stroke; render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order; render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let fill_and_stroke = row let mut style = row.element.style.clone();
.element if needs_separate_fill {
.style style.clear_fill();
.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params); }
let fill_and_stroke = style.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params);
if let Some((id, mask_type, _)) = push_id { if let Some((id, mask_type, _)) = push_id {
let selector = format!("url(#{id})"); let selector = format!("url(#{id})");
@@ -761,6 +800,28 @@ impl Render for Table<Vector> {
attributes.push("style", row.alpha_blending.blend_mode.render()); attributes.push("style", row.alpha_blending.blend_mode.render());
} }
}); });
// When splitting passes and stroke is below, draw the fill after the stroke.
if needs_separate_fill && wants_stroke_below {
render.leaf_tag("path", |attributes| {
attributes.push("d", path);
let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let mut style = row.element.style.clone();
style.clear_stroke();
let fill_and_stroke = style.render(
&mut attributes.0.svg_defs,
element_transform,
applied_stroke_transform,
bounds_matrix,
transformed_bounds_matrix,
&render_params,
);
attributes.push_val(fill_and_stroke);
});
}
} }
} }
@@ -800,8 +861,8 @@ impl Render for Table<Vector> {
let opacity = row.alpha_blending.opacity(render_params.for_mask); let opacity = row.alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() { if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() {
layer = true; layer = true;
let weight = row.element.style.stroke().unwrap().weight; let weight = row.element.style.stroke().unwrap().effective_width();
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant()); let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
let layer_bounds = quad.bounding_box(); let layer_bounds = quad.bounding_box();
scene.push_layer( scene.push_layer(
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
@@ -814,30 +875,98 @@ impl Render for Table<Vector> {
let can_draw_aligned_stroke = let can_draw_aligned_stroke =
row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed()); row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed());
let reorder_for_outside = row.element.style.stroke().is_some_and(|stroke| stroke.align == StrokeAlign::Outside) && !row.element.style.fill().is_none(); let use_layer = can_draw_aligned_stroke;
let use_layer = can_draw_aligned_stroke && !reorder_for_outside; let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == graphene_core::vector::style::PaintOrder::StrokeBelow);
if use_layer {
let mut element = row.element.clone();
element.style.clear_stroke();
element.style.set_fill(Fill::solid(Color::BLACK));
let vector_table = Table::new_from_row(TableRow { // Closures to avoid duplicated fill/stroke drawing logic
element, let do_fill = |scene: &mut Scene| match row.element.style.fill() {
alpha_blending: *row.alpha_blending, Fill::Solid(color) => {
transform: *row.transform, let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
source_node_id: None, scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
}); }
Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new();
for &(offset, color) in &gradient.stops {
stops.push(peniko::ColorStop {
offset: offset as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}
let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let bounds = row.element.nonzero_bounding_box();
let weight = row.element.style.stroke().unwrap().weight; let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let quad = Quad::from_box(bounds).inflate(weight * element_transform.matrix2.determinant());
let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); parent_transform.inverse()
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcIn), 1., kurbo::Affine::IDENTITY, &rect); } else {
} Default::default()
};
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
let start = mod_points.transform_point2(gradient.start);
let end = mod_points.transform_point2(gradient.end);
let fill = peniko::Brush::Gradient(peniko::Gradient {
kind: match gradient.gradient_type {
GradientType::Linear => peniko::GradientKind::Linear {
start: to_point(start),
end: to_point(end),
},
GradientType::Radial => {
let radius = start.distance(end);
peniko::GradientKind::Radial {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
}
},
stops,
..Default::default()
});
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
}
Fill::None => {}
};
let do_stroke = |scene: &mut Scene, width_scale: f64| {
if let Some(stroke) = row.element.style.stroke() {
let color = match stroke.color {
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
None => peniko::Color::TRANSPARENT,
};
let cap = match stroke.cap {
StrokeCap::Butt => Cap::Butt,
StrokeCap::Round => Cap::Round,
StrokeCap::Square => Cap::Square,
};
let join = match stroke.join {
StrokeJoin::Miter => Join::Miter,
StrokeJoin::Bevel => Join::Bevel,
StrokeJoin::Round => Join::Round,
};
let stroke = kurbo::Stroke {
width: stroke.weight * width_scale,
miter_limit: stroke.join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern: stroke.dash_lengths.into(),
dash_offset: stroke.dash_offset,
};
if stroke.width > 0. {
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
}
}
};
// Render the path // Render the path
match render_params.view_mode { match render_params.view_mode {
@@ -861,120 +990,76 @@ impl Render for Table<Vector> {
scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path); scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path);
} }
_ => { _ => {
enum Op { if use_layer {
Fill, let mut element = row.element.clone();
Stroke, element.style.clear_stroke();
} element.style.set_fill(Fill::solid(Color::BLACK));
let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside { let vector_table = Table::new_from_row(TableRow {
true => [Op::Stroke, Op::Fill], element,
false => [Op::Fill, Op::Stroke], // Default alpha_blending: *row.alpha_blending,
}; transform: *row.transform,
source_node_id: None,
});
for operation in order { let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
match operation { let weight = row.element.style.stroke().unwrap().effective_width();
Op::Fill => { let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform));
match row.element.style.fill() { let bounds = quad.bounding_box();
Fill::Solid(color) => { let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
}
Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new();
for &(offset, color) in &gradient.stops {
stops.push(peniko::ColorStop {
offset: offset as f32,
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
});
}
// Compute bounding box of the shape to determine the gradient start and end points
let bounds = row.element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
parent_transform.inverse() peniko::Compose::SrcOut
} else { } else {
Default::default() peniko::Compose::SrcIn
}; };
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
let start = mod_points.transform_point2(gradient.start); if wants_stroke_below {
let end = mod_points.transform_point2(gradient.end); scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
let fill = peniko::Brush::Gradient(peniko::Gradient { do_stroke(scene, 2.);
kind: match gradient.gradient_type {
GradientType::Linear => peniko::GradientKind::Linear {
start: to_point(start),
end: to_point(end),
},
GradientType::Radial => {
let radius = start.distance(end);
peniko::GradientKind::Radial {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: radius as f32,
}
}
},
stops,
..Default::default()
});
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
let inverse_element_transform = if element_transform.matrix2.determinant() != 0. {
element_transform.inverse()
} else {
Default::default()
};
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
}
Fill::None => {}
};
}
Op::Stroke => {
if let Some(stroke) = row.element.style.stroke() {
let color = match stroke.color {
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
None => peniko::Color::TRANSPARENT,
};
let cap = match stroke.cap {
StrokeCap::Butt => Cap::Butt,
StrokeCap::Round => Cap::Round,
StrokeCap::Square => Cap::Square,
};
let join = match stroke.join {
StrokeJoin::Miter => Join::Miter,
StrokeJoin::Bevel => Join::Bevel,
StrokeJoin::Round => Join::Round,
};
let stroke = kurbo::Stroke {
width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. },
miter_limit: stroke.join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern: stroke.dash_lengths.into(),
dash_offset: stroke.dash_offset,
};
// Draw the stroke if it's visible scene.pop_layer();
if stroke.width > 0. { scene.pop_layer();
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
} do_fill(scene);
} } else {
// Fill first (unclipped), then stroke (clipped) above
do_fill(scene);
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
do_stroke(scene, 2.);
scene.pop_layer();
scene.pop_layer();
}
} else {
// Non-aligned strokes or open paths: default order behavior
enum Op {
Fill,
Stroke,
}
let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) {
true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default
};
for operation in order {
match operation {
Op::Fill => do_fill(scene),
Op::Stroke => do_stroke(scene, 1.),
} }
} }
} }
} }
} }
if use_layer {
scene.pop_layer();
scene.pop_layer();
}
// If we pushed a layer for opacity or a blend mode, we need to pop it // If we pushed a layer for opacity or a blend mode, we need to pop it
if layer { if layer {
scene.pop_layer(); scene.pop_layer();
@@ -1028,7 +1113,7 @@ impl Render for Table<Vector> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for row in self.iter() { for row in self.iter() {
let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::weight); let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let filled = row.element.style.fill() != &Fill::None; let filled = row.element.style.fill() != &Fill::None;
let fill = |mut subpath: Subpath<_>| { let fill = |mut subpath: Subpath<_>| {
if filled { if filled {
+1
View File
@@ -7,6 +7,7 @@ license = "MIT OR Apache-2.0"
[features] [features]
[dependencies] [dependencies]
log = { workspace = true }
# Workspace dependencies # Workspace dependencies
graphene-std = { workspace = true, features = ["gpu"] } graphene-std = { workspace = true, features = ["gpu"] }
+11 -1
View File
@@ -1,3 +1,6 @@
#[macro_use]
extern crate log;
use graph_craft::document::value::*; use graph_craft::document::value::*;
use graph_craft::document::*; use graph_craft::document::*;
use graph_craft::proto::RegistryValueSource; use graph_craft::proto::RegistryValueSource;
@@ -150,7 +153,14 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp
match field.value_source { match field.value_source {
RegistryValueSource::None => {} RegistryValueSource::None => {}
RegistryValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed), RegistryValueSource::Default(data) => {
if let Some(custom_default) = TaggedValue::from_primitive_string(data, ty) {
return NodeInput::value(custom_default, exposed);
} else {
// It is incredibly useful to get a warning when the default type cannot be parsed rather than defaulting to `()`.
warn!("Failed to parse default value for type {ty:?} with data {data}");
}
}
RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)), RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)),
}; };