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
run: |
mold -run cargo test --all-features --workspace
mold -run cargo test --all-features
- name: 📃 Generate code documentation info for website
if: github.ref == 'refs/heads/master'
+42 -4
View File
@@ -34,11 +34,47 @@
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" ];
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: {
version = "139.0.17";
gitRevision = "6c347eb";
@@ -51,7 +87,6 @@
strip $out/lib/*
'';
});
libcefPath = pkgs.runCommand "libcef-path" {} ''
mkdir -p $out
@@ -85,7 +120,7 @@
# Development tools that don't need to be in LD_LIBRARY_PATH
buildTools = [
rustc-wasm
rust
pkgs.nodejs
pkgs.nodePackages.npm
pkgs.binaryen
@@ -119,6 +154,9 @@
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";
RUST_GPU_PATH_OVERRIDE = rustGpuPathOverride;
RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so";
shellHook = ''
alias cargo='mold --run cargo'
'';
+4
View File
@@ -33,6 +33,10 @@
},
// Rust Analyzer config
"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.format.enable": true,
"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]
members = [
"editor",
"desktop",
"desktop/wrapper",
"proc-macros",
"desktop/embedded-resources",
"editor",
"frontend/wasm",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"node-graph/node-macro",
"node-graph/preprocessor",
"node-graph/wgpu-executor",
"proc-macros",
]
default-members = [
"desktop",
"desktop/wrapper",
"editor",
"frontend/wasm",
"libraries/dyn-any",
"libraries/path-bool",
"libraries/math-parser",
]
default-members = [
"editor",
"frontend/wasm",
"node-graph/gapplication-io",
"node-graph/gbrush",
"node-graph/gcore",
"node-graph/gcore-shaders",
"node-graph/gstd",
"node-graph/gmath-nodes",
"node-graph/gpath-bool",
"node-graph/graph-craft",
"node-graph/graphene-cli",
"node-graph/graster-nodes",
"node-graph/gstd",
"node-graph/gsvg-renderer",
"node-graph/interpreted-executor",
"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"
@@ -80,16 +92,16 @@ env_logger = "0.11"
log = "0.4"
bitflags = { version = "2.4", features = ["serde"] }
ctor = "0.2"
convert_case = "0.7"
convert_case = "0.8"
derivative = "2.2"
thiserror = "2"
anyhow = "1.0"
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = "1.0"
chrono = "0.4"
ron = "0.8"
ron = "0.11"
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
"fragile-send-sync-non-atomic-wasm",
"spirv",
@@ -120,9 +132,9 @@ web-sys = { version = "=0.3.77", features = [
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
url = "2.5"
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
resvg = "0.44"
usvg = "0.44"
vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made
resvg = "0.45"
usvg = "0.45"
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = [
@@ -136,9 +148,9 @@ image = { version = "0.25", default-features = false, features = [
"jpeg",
"bmp",
] }
parley = "0.5.0"
skrifa = "0.32.0"
pretty_assertions = "1.4.1"
parley = "0.5"
skrifa = "0.36"
pretty_assertions = "1.4"
fern = { version = "0.7", features = ["colored"] }
num_enum = "0.7"
num-derive = "0.4"
@@ -159,25 +171,25 @@ syn = { version = "2.0", default-features = false, features = [
"extra-traits",
"proc-macro",
] }
kurbo = { version = "0.11.0", features = ["serde"] }
kurbo = { version = "0.11", features = ["serde"] }
lyon_geom = "1.0"
petgraph = { version = "0.7.1", default-features = false, features = [
"graphmap",
] }
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
petgraph = { version = "0.7", default-features = false, features = ["graphmap"] }
half = { version = "2.4", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1", features = ["std"] }
criterion = { version = "0.5", features = ["html_reports"] }
iai-callgrind = { version = "0.12.3" }
ndarray = "0.16.1"
strum = { version = "0.26.3", features = ["derive"] }
criterion = { version = "0.7", features = ["html_reports"] }
iai-callgrind = { version = "0.16" }
ndarray = "0.16"
strum = { version = "0.27", features = ["derive"] }
dirs = "6.0"
cef = "139.0.1"
include_dir = "0.7.4"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
rfd = "0.15.4"
open = "5.3.2"
poly-cool = "0.2.0"
cef = "139.0"
include_dir = "0.7"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1"
rfd = "0.15"
open = "5.3"
poly-cool = "0.3"
spin = "0.10"
clap = "4.5"
[profile.dev]
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 = [
"RUSTSEC-2024-0370", # Unmaintained but still fully functional crate `proc-macro-error`
"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-2025-0014", # Unmaintained but still fully functional crate `humantime`
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# 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"
[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"]
# Hardware acceleration features
@@ -19,15 +21,15 @@ accelerated_paint_d3d11 = ["windows", "ash"]
accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"]
[dependencies]
# # Local dependencies
# Local dependencies
graphite-desktop-wrapper = { path = "wrapper" }
graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
include_dir = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { 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::render::FrameBufferRef;
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::{Arc, Mutex};
use std::time::Instant;
@@ -27,7 +30,6 @@ mod input;
mod internal;
mod ipc;
mod platform;
mod scheme_handler;
mod utility;
#[cfg(feature = "accelerated_paint")]
@@ -38,11 +40,12 @@ use texture_import::SharedTextureHandle;
pub(crate) use context::{CefContext, CefContextBuilder, InitError};
use winit::event_loop::EventLoopProxy;
pub(crate) trait CefEventHandler: Clone {
pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
fn window_size(&self) -> WindowSize;
fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>);
#[cfg(feature = "accelerated_paint")]
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
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
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)]
pub(crate) struct CefHandler {
window_size_receiver: Arc<Mutex<WindowSizeReceiver>>,
event_loop_proxy: EventLoopProxy<CustomEvent>,
wgpu_context: WgpuContext,
}
struct WindowSizeReceiver {
receiver: Receiver<WindowSize>,
window_size: WindowSize,
@@ -142,6 +167,73 @@ impl CefEventHandler for CefHandler {
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) {
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));
}
#[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 FRONTEND_DOMAIN: &str = "frontend";
pub(crate) const RESOURCE_SCHEME: &str = "resources";
pub(crate) const RESOURCE_DOMAIN: &str = "resources";
+17 -12
View File
@@ -6,20 +6,21 @@ use cef::{
use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext;
use crate::cef::CefHandler;
use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
use crate::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
pub(crate) struct CefContextBuilder {
pub(crate) struct CefContextBuilder<H: CefEventHandler> {
pub(crate) args: Args,
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 {
#[cfg(target_os = "macos")]
let _loader = {
@@ -34,7 +35,11 @@ impl CefContextBuilder {
let switch = CefString::from("type");
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 {
@@ -45,7 +50,7 @@ impl CefContextBuilder {
let cmd = self.args.as_cmd_line().unwrap();
let switch = CefString::from("type");
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());
if ret >= 0 {
SetupError::SubprocessFailed(process_type.to_string())
@@ -55,7 +60,7 @@ impl CefContextBuilder {
}
#[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 {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 0,
@@ -71,7 +76,7 @@ impl CefContextBuilder {
}
#[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 {
windowless_rendering_enabled: 1,
multi_threaded_message_loop: 1,
@@ -97,7 +102,7 @@ impl CefContextBuilder {
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 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
@@ -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 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 {
windowless_rendering_enabled: 1,
+4
View File
@@ -2,10 +2,14 @@ mod browser_process_app;
mod browser_process_client;
mod browser_process_handler;
mod browser_process_life_span_handler;
mod render_process_app;
mod render_process_handler;
mod render_process_v8_handler;
mod resource_handler;
mod scheme_handler_factory;
pub(super) mod render_handler;
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::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
use crate::cef::CefEventHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use super::browser_process_handler::BrowserProcessHandlerImpl;
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
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>) {
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>) {
@@ -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::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
use super::scheme_handler_factory::SchemeHandlerFactoryImpl;
use crate::cef::CefEventHandler;
use crate::cef::consts::GRAPHITE_SCHEME;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::consts::RESOURCE_SCHEME;
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
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> {
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) {
+1 -1
View File
@@ -9,7 +9,6 @@ pub(crate) struct RenderHandlerImpl<H: CefEventHandler> {
object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H,
}
impl<H: CefEventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> Self {
Self {
@@ -18,6 +17,7 @@ impl<H: CefEventHandler> RenderHandlerImpl<H> {
}
}
}
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut 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 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>,
render_process_handler: RenderProcessHandler,
}
impl RenderProcessAppImpl {
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
pub(crate) fn app() -> App {
App::new(Self {
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>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
}
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 {
unsafe {
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 {
unsafe {
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>) {
self.object = object;
}
@@ -5,7 +5,6 @@ use crate::cef::ipc::{MessageType, SendMessage};
pub struct BrowserProcessV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
}
impl BrowserProcessV8HandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
@@ -72,7 +71,6 @@ impl Clone for BrowserProcessV8HandlerImpl {
Self { object: self.object }
}
}
impl Rc for BrowserProcessV8HandlerImpl {
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
unsafe {
@@ -81,7 +79,6 @@ impl Rc for BrowserProcessV8HandlerImpl {
}
}
}
impl WrapV8Handler for BrowserProcessV8HandlerImpl {
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
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::time::Instant;
use cef::CefHandler;
use tracing_subscriber::EnvFilter;
use winit::event_loop::EventLoop;
@@ -30,7 +31,7 @@ pub(crate) enum CustomEvent {
fn main() {
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() {
// We are in a CEF subprocess
-1
View File
@@ -16,7 +16,6 @@ gpu = ["graphite-editor/gpu"]
# Local dependencies
graphite-editor = { path = "../../editor", features = [
"gpu",
"ron",
"vello",
] }
graphene-std = { workspace = true }
+1 -4
View File
@@ -16,7 +16,6 @@ wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"]
ron = []
[dependencies]
# Local dependencies
@@ -45,9 +44,7 @@ once_cell = { workspace = true }
web-sys = { workspace = true }
vello = { workspace = true }
base64 = { workspace = true }
# Required dependencies
spin = "0.9.8"
spin = { workspace = true }
# Optional local dependencies
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::prelude::*;
use crate::messages::tool::utility_types::HintData;
use glam::IVec2;
use graph_craft::document::NodeId;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
@@ -124,12 +125,19 @@ pub enum FrontendMessage {
document_id: DocumentId,
},
UpdateImportsExports {
imports: Vec<(FrontendGraphOutput, i32, i32)>,
exports: Vec<(FrontendGraphInput, i32, i32)>,
#[serde(rename = "addImport")]
add_import: Option<(i32, i32)>,
#[serde(rename = "addExport")]
add_export: Option<(i32, i32)>,
/// If the primary import is not visible, then it is None.
imports: Vec<Option<FrontendGraphOutput>>,
/// If the primary export is not visible, then it is None.
exports: Vec<Option<FrontendGraphInput>>,
/// The primary import location.
#[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 {
#[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::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::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::utility_types::PanelType;
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 {
return;
}
@@ -2776,6 +2776,7 @@ impl DocumentMessageHandler {
let selected_layer = selected_layers.next();
let has_selection = selected_layer.is_some();
let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let widgets = vec![
PopoverButton::new()
@@ -2789,7 +2790,7 @@ impl DocumentMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
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()))
} else {
None
@@ -382,7 +382,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
@@ -771,6 +770,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::node(NodeId(0), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
@@ -825,8 +825,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Image", "TODO").into()],
output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
has_primary_output: false,
output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
@@ -883,7 +882,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
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: [
DocumentNode {
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()
}),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Vec2", "TODO").into()],
output_names: vec!["X".to_string(), "Y".to_string()],
has_primary_output: false,
output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
@@ -77,7 +77,6 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
},
@@ -18,7 +18,11 @@ pub enum NodeGraphMessage {
},
AddPathNode,
AddImport,
AddPrimaryImport,
AddSecondaryImport,
AddExport,
AddPrimaryExport,
AddSecondaryExport,
Init,
SelectedNodesUpdated,
Copy,
@@ -63,6 +67,12 @@ pub enum NodeGraphMessage {
set_to_exposed: bool,
start_transaction: bool,
},
ExposeEncapsulatingPrimaryInput {
exposed: bool,
},
ExposePrimaryExport {
exposed: bool,
},
InsertNode {
node_id: NodeId,
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 crate::consts::GRID_SIZE;
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] });
}
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(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);
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 => {
network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path);
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 => {
responses.add(BroadcastMessage::SubscribeEvent {
on: EventMessage::SelectionChanged,
@@ -401,6 +435,77 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
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 } => {
network_interface.insert_node(node_id, node_template, selection_network_path);
}
@@ -706,21 +811,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
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(NodeGraphMessage::AddExport);
return;
} else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() {
responses.add(DocumentMessage::AddTransaction);
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 });
if remove_import_index == 0 {
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false })
} else {
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
}
return;
} 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(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;
} 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);
@@ -756,10 +861,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
// Alt-click sets the clicked node as previewed
if alt_click {
if let Some(clicked_node) = clicked_id {
self.preview_on_mouse_up = Some(clicked_node);
}
if alt_click && let Some(clicked_node) = clicked_id {
self.preview_on_mouse_up = Some(clicked_node);
}
// Begin moving an existing wire
@@ -785,14 +888,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.initial_disconnecting = false;
self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path);
if let Some((output_type, source)) = clicked_output
.node_id()
.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;
}
let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path);
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source);
self.update_node_graph_hints(responses);
return;
@@ -1023,7 +1120,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
(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 });
} 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)
})
.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 });
}
@@ -1098,27 +1197,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::RunDocumentGraph);
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 self.context_menu.is_some() {
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 {
warn!("No network_metadata");
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 {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())),
}
})
});
let compatible_type = match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => 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_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;
@@ -1442,11 +1541,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::RemoveExport { export_index: usize } => {
network_interface.remove_export(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph);
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 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;
};
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 => {
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
let add_import = network_interface
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
let add_export = network_interface
.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();
let imports = network_interface.frontend_imports(breadcrumb_network_path);
let exports = network_interface.frontend_exports(breadcrumb_network_path);
let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else {
log::error!("Could not get import export positions");
return;
};
// Do not show the add import or add export button in the document network;
let add_import_export = !breadcrumb_network_path.is_empty();
responses.add(NodeGraphMessage::UpdateVisibleNodes);
responses.add(NodeGraphMessage::SendWires);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
import_position,
export_position,
add_import_export,
});
}
@@ -1990,11 +2087,6 @@ impl NodeGraphMessageHandler {
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 {
warn!("No selected nodes in update_selection_action_buttons");
return;
@@ -2008,6 +2100,7 @@ impl NodeGraphMessageHandler {
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let selected_layer = selected_layers.next();
let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let mut widgets = vec![
PopoverButton::new()
@@ -2020,7 +2113,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
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()))
} else {
None
@@ -2106,6 +2199,11 @@ impl NodeGraphMessageHandler {
let mut selection = selected_nodes.selected_nodes();
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 { .. }) {
network.exports.iter().find_map(|export| {
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 node_type = graph_layer.horizontal_layer_flow().nth(1);
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()))
} else {
None
@@ -2426,110 +2524,48 @@ impl NodeGraphMessageHandler {
}
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 {
log::error!("Could not get nested network when collecting nodes");
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();
for (&node_id, node) in &network.nodes {
let node_id_path = [breadcrumb_network_path, (&[node_id])].concat();
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 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| {
input.map(|input| FrontendGraphInput {
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source),
resolved_type: format!("{:?}", &input.ty),
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(),
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,
})
let primary_input = if network_interface
.input_from_connector(&primary_input_connector, breadcrumb_network_path)
.is_some_and(|input| input.is_exposed())
{
network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path)
} else {
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();
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 primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else {
log::error!("Could not get node_metadata when getting output for {node_id}");
continue;
};
let output_name = node_metadata
.persistent_metadata
.output_names
.get(output_index)
.cloned()
.filter(|output_name| !output_name.is_empty())
.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 exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
.filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
.collect();
let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) {
(
network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path),
network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path),
)
} else {
(false, false)
};
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 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}");
continue;
};
@@ -2555,19 +2591,20 @@ impl NodeGraphMessageHandler {
is_layer: network_interface
.node_metadata(&node_id, breadcrumb_network_path)
.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(),
display_name: network_interface.display_name(&node_id, breadcrumb_network_path),
primary_input,
exposed_inputs,
primary_output,
exposed_outputs,
primary_output_connected_to_layer,
primary_input_connected_to_layer,
position,
previewed,
visible: node.visible,
visible,
locked,
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 {
fn default() -> 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::<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::<Color>() => color_widget(default_info, ColorInput::default()),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default()),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
@@ -926,6 +928,22 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input
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(
color_button
.value(match color_table.iter().next() {
@@ -965,7 +983,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
.on_commit(commit_value)
.widget_holder(),
),
_ => {}
x => warn!("Colour {x:?}"),
}
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::value::TaggedValue;
use graphene_std::Type;
@@ -60,7 +61,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "validTypes")]
pub valid_types: Vec<String>,
#[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)]
@@ -68,11 +70,13 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
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")]
pub connected_to: Vec<InputConnector>,
pub connected_to: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -93,13 +97,15 @@ pub struct FrontendNode {
pub primary_output: Option<FrontendGraphOutput>,
#[serde(rename = "exposedOutputs")]
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 locked: bool,
pub previewed: bool,
pub errors: Option<String>,
#[serde(rename = "uiOnly")]
pub ui_only: bool,
}
#[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",
],
},
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 {
node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
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;
(!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, ());
@@ -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 {
self.document_ids.push_front(document_id);
} else {
@@ -1,8 +1,8 @@
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*;
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)
}
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
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()?;
if selected_layers.next().is_some() {
return None;
}
for _ in selected_layers {}
// Must be a layer of type Table<Vector>
let compatible_type = NodeGraphLayer::new(first_layer, network_interface)
.horizontal_layer_flow()
.nth(1)
.map(|node_id| {
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 {
let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?;
let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
if output_type.nested_type() != concrete!(Table<Vector>).nested_type() {
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::tool::common_functionality::auto_panning::AutoPanning;
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::snapping;
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 graph_craft::document::NodeId;
use graphene_std::Artboard;
use graphene_std::renderer::Quad;
use graphene_std::renderer::{Quad, Rect};
use graphene_std::table::Table;
#[derive(Default, ExtractField)]
@@ -241,6 +242,39 @@ impl Fsm for ArtboardToolFsmState {
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);
self
@@ -3171,7 +3171,7 @@ impl Fsm for PathToolFsmState {
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_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 type { Editor } from "@graphite/editor";
import type { Node } from "@graphite/messages";
import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons";
@@ -178,7 +177,7 @@
}
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 {
@@ -189,37 +188,11 @@
function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) return "Connected to nothing";
return output.connectedTo
.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");
return `Connected to:\n${output.connectedTo.join("\n")}`;
}
function inputConnectedToText(input: FrontendGraphInput): string {
if (input.connectedTo === undefined) return "Connected to nothing";
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;
return `Connected to:\n${input.connectedTo}`;
}
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
@@ -265,16 +238,12 @@
{
value: "node",
label: "Node",
action: () => {
toggleLayerDisplay(false, contextMenuData.nodeId);
},
action: () => toggleLayerDisplay(false, contextMenuData.nodeId),
},
{
value: "layer",
label: "Layer",
action: () => {
toggleLayerDisplay(true, contextMenuData.nodeId);
},
action: () => toggleLayerDisplay(true, contextMenuData.nodeId),
},
]}
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
@@ -334,149 +303,181 @@
<!-- 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})`}>
{#each $nodeGraph.imports as { outputMetadata, position }, index}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
data-connector="output"
data-datatype={outputMetadata.dataType}
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
<title>{`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`}</title>
{#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)" />
{: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)" />
{/if}
</svg>
{#if $nodeGraph.updateImportsExports}
{#each $nodeGraph.updateImportsExports.imports as frontendOutput, index}
{#if frontendOutput}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
data-connector="output"
data-datatype={frontendOutput.dataType}
style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
>
<title>{`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`}</title>
{#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)" />
{: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)" />
{/if}
</svg>
<div
class="edit-import-export import"
on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerleave={() => (hoveringImportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if editingNameImportIndex == index}
<input
class="import-text-input"
type="text"
style:width={importsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
<div
on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerleave={() => (hoveringImportIndex = undefined)}
class="edit-import-export import"
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
>
{#if editingNameImportIndex == index}
<input
class="import-text-input"
type="text"
style:width={importsToEdgeTextInputWidth()}
bind:this={inputElement}
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}
<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 hoveringImportIndex === index || editingNameImportIndex === index}
<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 */
}}
/>
<div class="reorder-drag-grip" title="Reorder this import"></div>
{/if}
</div>
{/each}
{#if $nodeGraph.reorderImportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.imports[0].position.x),
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#if $nodeGraph.addImport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
<IconButton
size={24}
icon="Add"
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if}
{#each $nodeGraph.exports as { inputMetadata, position }, index}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
data-connector="input"
data-datatype={inputMetadata.dataType}
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
<title>{`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`}</title>
{#if inputMetadata.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)" />
{/each}
{#each $nodeGraph.updateImportsExports.exports as frontendInput, index}
{#if frontendInput}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
class="connector"
data-connector="input"
data-datatype={frontendInput.dataType}
style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
>
<title>{`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`}</title>
{#if frontendInput.connectedTo !== "nothing"}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{: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)" />
{/if}
</svg>
<div
on:pointerenter={() => (hoveringExportIndex = index)}
on:pointerleave={() => (hoveringExportIndex = undefined)}
class="edit-import-export export"
class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
>
{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{#if index > 0}
<div class="reorder-drag-grip" title="Reorder this export" />
{/if}
<IconButton
size={16}
icon={"Remove"}
class="remove-button-export"
data-index={index}
data-export-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{/if}
{#if editingNameExportIndex === index}
<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}
<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}
</svg>
<div
class="edit-import-export export"
on:pointerenter={() => (hoveringExportIndex = index)}
on:pointerleave={() => (hoveringExportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if hoveringExportIndex === index || editingNameExportIndex === index}
<div class="reorder-drag-grip" title="Reorder this export"></div>
<IconButton
size={16}
icon={"Remove"}
class="remove-button-export"
data-index={index}
data-export-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{/if}
{#if editingNameExportIndex === index}
<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, inputMetadata.name)}>{inputMetadata.name}</p>
{/if}
</div>
{/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>
{/each}
{#if $nodeGraph.updateImportsExports.addImportExport == true}
<div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
</div>
<div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
>
<IconButton size={24} icon={"Add"} action={() => editor.handle.addSecondaryExport()} />
</div>
{/if}
{#if $nodeGraph.reorderImportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.updateImportsExports.importPosition.x),
y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24} />
{/if}
{#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.updateImportsExports.exportPosition.x),
y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24} />
{/if}
{/if}
</div>
@@ -530,7 +531,7 @@
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#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)" />
{#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)" />
{/if}
{:else}
@@ -551,9 +552,9 @@
{#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{/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)" />
{#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)" />
{/if}
{:else}
@@ -937,6 +938,24 @@
margin-top: -5px;
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 {
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 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]) }));
@@ -58,17 +52,17 @@ export class UpdateContextMenuInformation extends JsMessage {
}
export class UpdateImportsExports extends JsMessage {
@ImportsToVec2Array
readonly imports!: { outputMetadata: FrontendGraphOutput; position: XY }[];
readonly imports!: (FrontendGraphOutput | undefined)[];
@ExportsToVec2Array
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[];
readonly exports!: (FrontendGraphInput | undefined)[];
@TupleToVec2
readonly addImport!: XY | undefined;
readonly importPosition!: XY;
@TupleToVec2
readonly addExport!: XY | undefined;
readonly exportPosition!: XY;
readonly addImportExport!: boolean;
}
export class UpdateInSelectedNetwork extends JsMessage {
@@ -194,29 +188,6 @@ export type ContextMenuInformation = {
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 {
readonly dataType!: FrontendGraphDataType;
@@ -228,28 +199,9 @@ export class FrontendGraphInput {
readonly validTypes!: string[];
@CreateOutputConnectorOptional
connectedTo!: Node | undefined;
readonly connectedTo!: string;
}
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 {
readonly dataType!: FrontendGraphDataType;
@@ -259,8 +211,7 @@ export class FrontendGraphOutput {
readonly resolvedType!: string;
@CreateInputConnectorArray
connectedTo!: Node[];
readonly connectedTo!: string[];
}
export class FrontendNode {
@@ -274,20 +225,20 @@ export class FrontendNode {
readonly displayName!: string;
@Type(() => FrontendGraphInput)
readonly primaryInput!: FrontendGraphInput | undefined;
@Type(() => FrontendGraphInput)
readonly exposedInputs!: FrontendGraphInput[];
@Type(() => FrontendGraphOutput)
readonly primaryOutput!: FrontendGraphOutput | undefined;
@Type(() => FrontendGraphOutput)
readonly exposedOutputs!: FrontendGraphOutput[];
readonly primaryInputConnectedToLayer!: boolean;
readonly primaryOutputConnectedToLayer!: boolean;
@TupleToVec2
readonly position!: XY | undefined;
readonly position!: XY;
// TODO: Store field for the width of the left node chain
@@ -298,8 +249,6 @@ export class FrontendNode {
readonly unlocked!: boolean;
readonly errors!: string | undefined;
readonly uiOnly!: boolean;
}
export class FrontendNodeType {
+2 -9
View File
@@ -1,7 +1,6 @@
import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor";
import type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/messages";
import {
type Box,
type FrontendClickTargets,
@@ -37,10 +36,7 @@ export function createNodeGraphState(editor: Editor) {
layerWidths: new Map<bigint, number>(),
chainWidths: new Map<bigint, number>(),
hasLeftInputWire: new Map<bigint, boolean>(),
imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[],
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,
updateImportsExports: undefined as UpdateImportsExports | undefined,
nodes: new Map<bigint, FrontendNode>(),
visibleNodes: new Set<bigint>(),
/// 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) => {
update((state) => {
state.imports = updateImportsExports.imports;
state.exports = updateImportsExports.exports;
state.addImport = updateImportsExports.addImport;
state.addExport = updateImportsExports.addExport;
state.updateImportsExports = updateImportsExports;
return state;
});
});
+24
View File
@@ -314,6 +314,30 @@ impl EditorHandle {
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
#[wasm_bindgen(js_name = appWindowMaximize)]
pub fn app_window_maximize(&self) {
+1 -1
View File
@@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
[dependencies]
pest = "2.7"
pest_derive = "2.7.11"
pest_derive = "2.7"
thiserror = "2.0"
lazy_static = "1.5"
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.
#[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 {
#[default]
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]),
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.
+33 -4
View File
@@ -942,6 +942,35 @@ async fn solidify_stroke(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
.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))]
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Vector>)] content: Table<I>) -> Table<Vector>
where
@@ -1065,11 +1094,11 @@ async fn sample_polyline(
.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.
#[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 bezpaths = content
@@ -1108,9 +1137,9 @@ async fn split_path(_: impl Ctx, mut content: Table<Vector>, progress: Fraction,
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))]
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
for row in content.iter_mut() {
let points_count = row.element.point_domain.ids().len();
@@ -171,6 +171,8 @@ tagged_value! {
Bool(bool),
String(String),
OptionalF64(Option<f64>),
ColorNotInTable(Color),
OptionalColorNotInTable(Option<Color>),
// ========================
// 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::<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::<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::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => 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 }
wgpu = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
# Required dependencies
clap = { version = "4.5.31", features = ["cargo", "derive"] }
clap = { workspace = true, features = ["cargo", "derive"] }
# Optional local dependencies
wgpu-executor = { path = "../wgpu-executor", optional = true }
+1 -4
View File
@@ -307,7 +307,7 @@ fn black_and_white<T: Adjust<Color>>(
GradientStops,
)]
mut image: T,
#[default(Color::BLACK)] tint: Table<Color>,
#[default(Color::BLACK)] tint: Color,
#[default(40.)]
#[range((-200., 300.))]
reds: PercentageF32,
@@ -327,9 +327,6 @@ fn black_and_white<T: Adjust<Color>>(
#[range((-200., 300.))]
magentas: PercentageF32,
) -> T {
let tint: Option<Color> = tint.into();
let tint = tint.unwrap_or(Color::BLACK);
image.adjust(|color| {
let color = color.to_gamma_srgb();
+2 -11
View File
@@ -166,15 +166,12 @@ fn color_overlay<T: Adjust<Color>>(
GradientStops,
)]
mut image: T,
#[default(Color::BLACK)] color: Table<Color>,
#[default(Color::BLACK)] color: Color,
blend_mode: BlendMode,
#[default(100.)] opacity: PercentageF32,
) -> T {
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| {
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
let opacity = 100.;
let result = super::color_overlay(
(),
Table::new_from_element(Raster::new_cpu(image.clone())),
Table::new_from_element(overlay_color),
BlendMode::Multiply,
opacity,
);
let result = super::color_overlay((), Table::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity);
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)
+2 -6
View File
@@ -17,12 +17,8 @@ fn text<'i: 'n>(
#[unit(" px")]
#[default(0.)]
character_spacing: f64,
#[unit(" px")]
#[default(None)]
max_width: Option<f64>,
#[unit(" px")]
#[default(None)]
max_height: Option<f64>,
#[unit(" px")] max_width: Option<f64>,
#[unit(" px")] max_height: Option<f64>,
/// Faux italic.
#[unit("°")]
#[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::vector::Vector;
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 kurbo::Affine;
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 {
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)
@@ -690,11 +696,36 @@ impl Render for Table<Vector> {
} else {
MaskType::Mask
};
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_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 mut element = row.element.clone();
@@ -708,13 +739,11 @@ impl Render for Table<Vector> {
source_node_id: None,
});
Some((id, mask_type, vector_row))
} else {
None
};
(id, mask_type, vector_row)
});
render.leaf_tag("path", |attributes| {
attributes.push("d", path);
attributes.push("d", path.clone());
let matrix = format_transform_matrix(element_transform);
if !matrix.is_empty() {
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 {
let mut svg = SvgRender::new();
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let weight = row.element.style.stroke().unwrap().weight * row.transform.matrix2.determinant();
let stroke = row.element.style.stroke().unwrap();
let weight = stroke.effective_width() * max_scale(applied_stroke_transform);
let quad = Quad::from_box(transformed_bounds).inflate(weight);
let (x, y) = quad.top_left().into();
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
let rect = format!(r##"<rect x="{x}" y="{y}" width="{width}" height="{height}" fill="white" />"##);
match mask_type {
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.override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let fill_and_stroke = row
.element
.style
.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params);
let mut style = row.element.style.clone();
if needs_separate_fill {
style.clear_fill();
}
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 {
let selector = format!("url(#{id})");
@@ -761,6 +800,28 @@ impl Render for Table<Vector> {
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);
if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() {
layer = true;
let weight = row.element.style.stroke().unwrap().weight;
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant());
let weight = row.element.style.stroke().unwrap().effective_width();
let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
let layer_bounds = quad.bounding_box();
scene.push_layer(
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
@@ -814,30 +875,98 @@ impl Render for Table<Vector> {
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());
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 && !reorder_for_outside;
if use_layer {
let mut element = row.element.clone();
element.style.clear_stroke();
element.style.set_fill(Fill::solid(Color::BLACK));
let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == graphene_core::vector::style::PaintOrder::StrokeBelow);
let vector_table = Table::new_from_row(TableRow {
element,
alpha_blending: *row.alpha_blending,
transform: *row.transform,
source_node_id: None,
});
// Closures to avoid duplicated fill/stroke drawing logic
let do_fill = |scene: &mut Scene| match row.element.style.fill() {
Fill::Solid(color) => {
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()])),
});
}
let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
let weight = row.element.style.stroke().unwrap().weight;
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);
let bounds = row.element.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
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, peniko::Compose::SrcIn), 1., kurbo::Affine::IDENTITY, &rect);
}
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} 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
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);
}
_ => {
enum Op {
Fill,
Stroke,
}
if use_layer {
let mut element = row.element.clone();
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 {
true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default
};
let vector_table = Table::new_from_row(TableRow {
element,
alpha_blending: *row.alpha_blending,
transform: *row.transform,
source_node_id: None,
});
for operation in order {
match operation {
Op::Fill => {
match row.element.style.fill() {
Fill::Solid(color) => {
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 bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
let weight = row.element.style.stroke().unwrap().effective_width();
let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform));
let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. {
parent_transform.inverse()
} else {
Default::default()
};
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
peniko::Compose::SrcOut
} else {
peniko::Compose::SrcIn
};
let start = mod_points.transform_point2(gradient.start);
let end = mod_points.transform_point2(gradient.end);
if wants_stroke_below {
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 {
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,
};
do_stroke(scene, 2.);
// Draw the stroke if it's visible
if stroke.width > 0. {
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
}
}
scene.pop_layer();
scene.pop_layer();
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 layer {
scene.pop_layer();
@@ -1028,7 +1113,7 @@ impl Render for Table<Vector> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
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 fill = |mut subpath: Subpath<_>| {
if filled {
+1
View File
@@ -7,6 +7,7 @@ license = "MIT OR Apache-2.0"
[features]
[dependencies]
log = { workspace = true }
# Workspace dependencies
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::*;
use graph_craft::proto::RegistryValueSource;
@@ -150,7 +153,14 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp
match field.value_source {
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)),
};