mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 17:58:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
@@ -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
@@ -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'
|
||||
'';
|
||||
|
||||
Vendored
+4
@@ -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
File diff suppressed because it is too large
Load Diff
+45
-33
@@ -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
|
||||
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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)'] }
|
||||
@@ -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?");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -16,7 +16,6 @@ gpu = ["graphite-editor/gpu"]
|
||||
# Local dependencies
|
||||
graphite-editor = { path = "../../editor", features = [
|
||||
"gpu",
|
||||
"ron",
|
||||
"vello",
|
||||
] }
|
||||
graphene-std = { workspace = true }
|
||||
|
||||
+1
-4
@@ -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: [
|
||||
|
||||
-1
@@ -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
+136
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,6 +7,7 @@ license = "MIT OR Apache-2.0"
|
||||
[features]
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
graphene-std = { workspace = true, features = ["gpu"] }
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user