Merge branch 'master' into grid_shape
@@ -65,11 +65,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748399823,
|
"lastModified": 1753238793,
|
||||||
"narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=",
|
"narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "d68a69dc71bc19beb3479800392112c2f6218159",
|
"rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -42,28 +42,44 @@
|
|||||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: {
|
||||||
|
version = "138.0.26";
|
||||||
|
gitRevision = "84f2d27";
|
||||||
|
chromiumVersion = "138.0.7204.158";
|
||||||
|
srcHash = "sha256-d9jQJX7rgdoHfROD3zmOdMSesRdKE3slB5ZV+U2wlbQ=";
|
||||||
|
|
||||||
|
__intentionallyOverridingVersion = true;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
strip $out/lib/*
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
|
libcefPath = pkgs.runCommand "libcef-path" {} ''
|
||||||
|
mkdir -p $out
|
||||||
|
|
||||||
|
ln -s ${libcef}/include $out/include
|
||||||
|
find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \;
|
||||||
|
find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \;
|
||||||
|
cp -r ${libcef}/share/cef/* $out/
|
||||||
|
|
||||||
|
echo '${builtins.toJSON {
|
||||||
|
type = "minimal";
|
||||||
|
name = builtins.baseNameOf libcef.src.url;
|
||||||
|
sha1 = "";
|
||||||
|
}}' > $out/archive.json
|
||||||
|
'';
|
||||||
|
|
||||||
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
|
# Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# System libraries
|
# System libraries
|
||||||
|
wayland
|
||||||
|
wayland.dev
|
||||||
openssl
|
openssl
|
||||||
vulkan-loader
|
vulkan-loader
|
||||||
mesa
|
mesa
|
||||||
libraw
|
libraw
|
||||||
|
libGL
|
||||||
|
|
||||||
# Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/#system-dependencies (under the NixOS tab)
|
|
||||||
at-spi2-atk
|
|
||||||
atkmm
|
|
||||||
cairo
|
|
||||||
gdk-pixbuf
|
|
||||||
glib
|
|
||||||
gtk3
|
|
||||||
harfbuzz
|
|
||||||
librsvg
|
|
||||||
libsoup_3
|
|
||||||
pango
|
|
||||||
webkitgtk_4_1
|
|
||||||
openssl
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Development tools that don't need to be in LD_LIBRARY_PATH
|
# Development tools that don't need to be in LD_LIBRARY_PATH
|
||||||
@@ -93,7 +109,6 @@
|
|||||||
gnuplot
|
gnuplot
|
||||||
samply
|
samply
|
||||||
cargo-flamegraph
|
cargo-flamegraph
|
||||||
|
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -101,11 +116,10 @@
|
|||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = buildInputs ++ buildTools ++ devTools;
|
packages = buildInputs ++ buildTools ++ devTools;
|
||||||
|
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
|
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}:${libcefPath}";
|
||||||
GIO_MODULE_DIR="${pkgs.glib-networking}/lib/gio/modules/";
|
CEF_PATH = libcefPath;
|
||||||
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
|
XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS";
|
||||||
|
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
alias cargo='mold --run cargo'
|
alias cargo='mold --run cargo'
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"editor",
|
"editor",
|
||||||
|
"desktop",
|
||||||
"proc-macros",
|
"proc-macros",
|
||||||
"frontend/wasm",
|
"frontend/wasm",
|
||||||
"frontend/src-tauri",
|
|
||||||
"node-graph/gapplication-io",
|
"node-graph/gapplication-io",
|
||||||
"node-graph/gbrush",
|
"node-graph/gbrush",
|
||||||
"node-graph/gcore",
|
"node-graph/gcore",
|
||||||
@@ -83,7 +83,7 @@ axum = "0.8"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
fastnoise-lite = "1.1"
|
fastnoise-lite = "1.1"
|
||||||
wgpu = { version = "23", features = [
|
wgpu = { version = "25.0.2", features = [
|
||||||
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
# We don't have wgpu on multiple threads (yet) https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#wgpu-types-now-send-sync-on-wasm
|
||||||
"fragile-send-sync-non-atomic-wasm",
|
"fragile-send-sync-non-atomic-wasm",
|
||||||
"spirv",
|
"spirv",
|
||||||
@@ -111,10 +111,10 @@ web-sys = { version = "=0.3.77", features = [
|
|||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
"ImageBitmapRenderingContext",
|
"ImageBitmapRenderingContext",
|
||||||
] }
|
] }
|
||||||
winit = "0.29"
|
winit = { version = "0.30", features = ["wayland", "rwh_06"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
|
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
|
||||||
vello = { git = "https://github.com/linebender/vello.git", rev = "3275ec8" } # TODO switch back to stable when a release is made
|
vello = { git = "https://github.com/linebender/vello.git" } # TODO switch back to stable when a release is made
|
||||||
resvg = "0.44"
|
resvg = "0.44"
|
||||||
usvg = "0.44"
|
usvg = "0.44"
|
||||||
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ accepted = [
|
|||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"NCSA",
|
"NCSA",
|
||||||
|
"bzip2-1.0.6",
|
||||||
]
|
]
|
||||||
workarounds = ["ring"]
|
workarounds = ["ring"]
|
||||||
ignore-build-dependencies = true
|
ignore-build-dependencies = true
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ targets = [
|
|||||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
# Tauri produces too many nonsense warnings.
|
|
||||||
exclude = ["tauri", "tauri-build"]
|
|
||||||
|
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check advisories`
|
# This section is considered when running `cargo deny check advisories`
|
||||||
# More documentation for the advisories section can be found here:
|
# More documentation for the advisories section can be found here:
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||||
@@ -85,6 +81,7 @@ allow = [
|
|||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016",
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"NCSA",
|
"NCSA",
|
||||||
|
"bzip2-1.0.6",
|
||||||
]
|
]
|
||||||
# The confidence threshold for detecting a license from license text.
|
# The confidence threshold for detecting a license from license text.
|
||||||
# The higher the value, the more closely the license text must be to the
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
# Generated by tauri
|
|
||||||
gen/
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "graphite-desktop"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Graphite Desktop"
|
||||||
|
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = ""
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.87"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["gpu"]
|
||||||
|
gpu = ["graphite-editor/gpu"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Local dependencies
|
||||||
|
graphite-editor = { path = "../editor", features = [
|
||||||
|
"gpu",
|
||||||
|
"ron",
|
||||||
|
"vello",
|
||||||
|
"decouple-execution",
|
||||||
|
] }
|
||||||
|
wgpu = { workspace = true }
|
||||||
|
winit = { workspace = true, features = ["serde"] }
|
||||||
|
base64.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
pollster = "0.3"
|
||||||
|
cef = "138.5.0"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
bytemuck = { version = "1.23.1", features = ["derive"] }
|
||||||
|
include_dir = "0.7.4"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use std::fs::metadata;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let frontend_dir = format!("{}/../frontend/dist", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
metadata(&frontend_dir).expect("Failed to find frontend directory. Please build the frontend first.");
|
||||||
|
metadata(format!("{}/index.html", &frontend_dir)).expect("Failed to find index.html in frontend directory.");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=.");
|
||||||
|
println!("cargo:rerun-if-changed=../frontend/dist");
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
use crate::CustomEvent;
|
||||||
|
use crate::WindowState;
|
||||||
|
use crate::WindowStateHandle;
|
||||||
|
use crate::render::GraphicsState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::event::StartCause;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
use winit::event_loop::ActiveEventLoop;
|
||||||
|
use winit::event_loop::ControlFlow;
|
||||||
|
use winit::window::Window;
|
||||||
|
use winit::window::WindowId;
|
||||||
|
|
||||||
|
use crate::cef;
|
||||||
|
|
||||||
|
pub(crate) struct WinitApp {
|
||||||
|
pub(crate) window_state: WindowStateHandle,
|
||||||
|
pub(crate) cef_context: cef::Context<cef::Initialized>,
|
||||||
|
pub(crate) window: Option<Arc<Window>>,
|
||||||
|
cef_schedule: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WinitApp {
|
||||||
|
pub(crate) fn new(window_state: WindowStateHandle, cef_context: cef::Context<cef::Initialized>) -> Self {
|
||||||
|
Self {
|
||||||
|
window_state,
|
||||||
|
cef_context,
|
||||||
|
window: None,
|
||||||
|
cef_schedule: Some(Instant::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||||
|
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let timeout = Instant::now() + Duration::from_millis(10);
|
||||||
|
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
|
||||||
|
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) {
|
||||||
|
if let Some(schedule) = self.cef_schedule
|
||||||
|
&& schedule < Instant::now()
|
||||||
|
{
|
||||||
|
self.cef_schedule = None;
|
||||||
|
self.cef_context.work();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
if let WindowState { width: Some(w), height: Some(h), .. } = s {
|
||||||
|
let window = Arc::new(
|
||||||
|
event_loop
|
||||||
|
.create_window(
|
||||||
|
Window::default_attributes()
|
||||||
|
.with_title("CEF Offscreen Rendering")
|
||||||
|
.with_inner_size(winit::dpi::LogicalSize::new(*w as u32, *h as u32)),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let graphics_state = pollster::block_on(GraphicsState::new(window.clone()));
|
||||||
|
|
||||||
|
self.window = Some(window.clone());
|
||||||
|
s.graphics_state = Some(graphics_state);
|
||||||
|
|
||||||
|
tracing::info!("Winit window created and ready");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
|
||||||
|
match event {
|
||||||
|
CustomEvent::UiUpdate => {
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomEvent::ScheduleBrowserWork(instant) => {
|
||||||
|
self.cef_schedule = Some(instant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||||
|
let Some(event) = self.cef_context.handle_window_event(event) else { return };
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
tracing::info!("The close button was pressed; stopping");
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(physical_size) => {
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
let width = physical_size.width as usize;
|
||||||
|
let height = physical_size.height as usize;
|
||||||
|
s.width = Some(width);
|
||||||
|
s.height = Some(height);
|
||||||
|
if let Some(graphics_state) = &mut s.graphics_state {
|
||||||
|
graphics_state.resize(width, height);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
self.cef_context.notify_of_resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
self.cef_context.work();
|
||||||
|
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
if let WindowState {
|
||||||
|
width: Some(width),
|
||||||
|
height: Some(height),
|
||||||
|
graphics_state: Some(graphics_state),
|
||||||
|
ui_frame_buffer: ui_fb,
|
||||||
|
..
|
||||||
|
} = s
|
||||||
|
{
|
||||||
|
if let Some(fb) = &*ui_fb {
|
||||||
|
graphics_state.update_texture(fb);
|
||||||
|
if fb.width() != *width && fb.height() != *height {
|
||||||
|
graphics_state.resize(*width, *height);
|
||||||
|
}
|
||||||
|
} else if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
match graphics_state.render() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
|
graphics_state.resize(*width, *height);
|
||||||
|
}
|
||||||
|
Err(wgpu::SurfaceError::OutOfMemory) => {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("{:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::FrameBuffer;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
mod input;
|
||||||
|
mod internal;
|
||||||
|
mod scheme_handler;
|
||||||
|
|
||||||
|
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
|
||||||
|
|
||||||
|
pub(crate) trait CefEventHandler: Clone {
|
||||||
|
fn window_size(&self) -> WindowSize;
|
||||||
|
fn draw(&self, frame_buffer: FrameBuffer) -> bool;
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct WindowSize {
|
||||||
|
pub(crate) width: usize,
|
||||||
|
pub(crate) height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowSize {
|
||||||
|
pub(crate) fn new(width: usize, height: usize) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use cef::sys::CEF_API_VERSION_LAST;
|
||||||
|
use cef::{App, BrowserSettings, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, RenderHandler, RequestContext, WindowInfo, browser_host_create_browser_sync, initialize};
|
||||||
|
use cef::{Browser, CefString, Settings, api_hash, args::Args, execute_process};
|
||||||
|
use thiserror::Error;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
|
||||||
|
use super::input::InputState;
|
||||||
|
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||||
|
use super::{CefEventHandler, input};
|
||||||
|
|
||||||
|
use super::internal::{AppImpl, ClientImpl, NonBrowserAppImpl, RenderHandlerImpl};
|
||||||
|
|
||||||
|
pub(crate) struct Setup {}
|
||||||
|
pub(crate) struct Initialized {}
|
||||||
|
pub(crate) trait ContextState {}
|
||||||
|
impl ContextState for Setup {}
|
||||||
|
impl ContextState for Initialized {}
|
||||||
|
|
||||||
|
pub(crate) struct Context<S: ContextState> {
|
||||||
|
args: Args,
|
||||||
|
pub(crate) browser: Option<Browser>,
|
||||||
|
pub(crate) input_state: InputState,
|
||||||
|
marker: std::marker::PhantomData<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context<Setup> {
|
||||||
|
pub(crate) fn new() -> Result<Context<Setup>, SetupError> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _loader = {
|
||||||
|
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
|
||||||
|
assert!(loader.load());
|
||||||
|
loader
|
||||||
|
};
|
||||||
|
let _ = api_hash(CEF_API_VERSION_LAST, 0);
|
||||||
|
|
||||||
|
let args = Args::new();
|
||||||
|
let cmd = args.as_cmd_line().unwrap();
|
||||||
|
let switch = CefString::from("type");
|
||||||
|
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
|
||||||
|
|
||||||
|
if !is_browser_process {
|
||||||
|
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
|
||||||
|
let mut app = NonBrowserAppImpl::app();
|
||||||
|
let ret = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
|
||||||
|
if ret >= 0 {
|
||||||
|
return Err(SetupError::SubprocessFailed(process_type.to_string()));
|
||||||
|
} else {
|
||||||
|
return Err(SetupError::Subprocess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Context {
|
||||||
|
args,
|
||||||
|
browser: None,
|
||||||
|
input_state: InputState::default(),
|
||||||
|
marker: std::marker::PhantomData::<Setup>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn init(self, event_handler: impl CefEventHandler) -> Result<Context<Initialized>, InitError> {
|
||||||
|
let settings = Settings {
|
||||||
|
windowless_rendering_enabled: 1,
|
||||||
|
multi_threaded_message_loop: 0,
|
||||||
|
external_message_pump: 1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults
|
||||||
|
let mut cef_app = App::new(AppImpl::new(event_handler.clone()));
|
||||||
|
|
||||||
|
let result = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
|
||||||
|
if result != 1 {
|
||||||
|
return Err(InitError::InitializationFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_handler = RenderHandlerImpl::new(event_handler.clone());
|
||||||
|
let mut client = Client::new(ClientImpl::new(RenderHandler::new(render_handler)));
|
||||||
|
|
||||||
|
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
|
||||||
|
|
||||||
|
let window_info = WindowInfo {
|
||||||
|
windowless_rendering_enabled: 1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = BrowserSettings {
|
||||||
|
windowless_frame_rate: 60,
|
||||||
|
background_color: 0x0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let browser = browser_host_create_browser_sync(
|
||||||
|
Some(&window_info),
|
||||||
|
Some(&mut client),
|
||||||
|
Some(&url),
|
||||||
|
Some(&settings),
|
||||||
|
Option::<&mut DictionaryValue>::None,
|
||||||
|
Option::<&mut RequestContext>::None,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Context {
|
||||||
|
args: self.args.clone(),
|
||||||
|
browser,
|
||||||
|
input_state: self.input_state.clone(),
|
||||||
|
marker: std::marker::PhantomData::<Initialized>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context<Initialized> {
|
||||||
|
pub(crate) fn work(&mut self) {
|
||||||
|
cef::do_message_loop_work();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_window_event(&mut self, event: WindowEvent) -> Option<WindowEvent> {
|
||||||
|
input::handle_window_event(self, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn notify_of_resize(&self) {
|
||||||
|
if let Some(browser) = &self.browser {
|
||||||
|
browser.host().unwrap().was_resized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ContextState> Drop for Context<S> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.browser.is_some() {
|
||||||
|
cef::shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum SetupError {
|
||||||
|
#[error("this is the sub process should exit immediately")]
|
||||||
|
Subprocess,
|
||||||
|
#[error("subprocess returned non zero exit code")]
|
||||||
|
SubprocessFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum InitError {
|
||||||
|
#[error("initialization failed")]
|
||||||
|
InitializationFailed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
|
||||||
|
use cef::{ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent};
|
||||||
|
use winit::dpi::PhysicalPosition;
|
||||||
|
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
|
|
||||||
|
use super::context::{Context, Initialized};
|
||||||
|
|
||||||
|
mod keymap;
|
||||||
|
use keymap::{ToDomBits, ToVKBits};
|
||||||
|
|
||||||
|
pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: WindowEvent) -> Option<WindowEvent> {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
if let Some(browser) = &context.browser {
|
||||||
|
if let Some(host) = browser.host() {
|
||||||
|
host.set_focus(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.input_state.update_mouse_position(&position);
|
||||||
|
let mouse_event: MouseEvent = (&context.input_state).into();
|
||||||
|
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
if let Some(browser) = &context.browser {
|
||||||
|
if let Some(host) = browser.host() {
|
||||||
|
host.set_focus(1);
|
||||||
|
|
||||||
|
let mouse_up = match state {
|
||||||
|
ElementState::Pressed => 0,
|
||||||
|
ElementState::Released => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cef_button = match button {
|
||||||
|
MouseButton::Left => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT)),
|
||||||
|
MouseButton::Right => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT)),
|
||||||
|
MouseButton::Middle => Some(cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE)),
|
||||||
|
MouseButton::Forward => None, //TODO: Handle Forward button
|
||||||
|
MouseButton::Back => None, //TODO: Handle Back button
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut mouse_state = context.input_state.mouse_state.clone();
|
||||||
|
match button {
|
||||||
|
MouseButton::Left => {
|
||||||
|
mouse_state.left = match state {
|
||||||
|
ElementState::Pressed => true,
|
||||||
|
ElementState::Released => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseButton::Right => {
|
||||||
|
mouse_state.right = match state {
|
||||||
|
ElementState::Pressed => true,
|
||||||
|
ElementState::Released => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseButton::Middle => {
|
||||||
|
mouse_state.middle = match state {
|
||||||
|
ElementState::Pressed => true,
|
||||||
|
ElementState::Released => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
context.input_state.update_mouse_state(mouse_state);
|
||||||
|
|
||||||
|
let mouse_event: MouseEvent = (&context.input_state).into();
|
||||||
|
|
||||||
|
if let Some(button) = cef_button {
|
||||||
|
host.send_mouse_click_event(
|
||||||
|
Some(&mouse_event),
|
||||||
|
button,
|
||||||
|
mouse_up,
|
||||||
|
1, // click count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => {
|
||||||
|
if let Some(browser) = &context.browser {
|
||||||
|
if let Some(host) = browser.host() {
|
||||||
|
let mouse_event = (&context.input_state).into();
|
||||||
|
let line_width = 40; //feels about right, TODO: replace with correct value
|
||||||
|
let line_height = 30; //feels about right, TODO: replace with correct value
|
||||||
|
let (delta_x, delta_y) = match delta {
|
||||||
|
MouseScrollDelta::LineDelta(x, y) => (x * line_width as f32, y * line_height as f32),
|
||||||
|
MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32),
|
||||||
|
};
|
||||||
|
host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
|
context.input_state.update_modifiers(&modifiers.state());
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => {
|
||||||
|
if let Some(browser) = &context.browser {
|
||||||
|
if let Some(host) = browser.host() {
|
||||||
|
host.set_focus(1);
|
||||||
|
|
||||||
|
let (named_key, character) = match &event.logical_key {
|
||||||
|
winit::keyboard::Key::Named(named_key) => (
|
||||||
|
Some(named_key),
|
||||||
|
match named_key {
|
||||||
|
winit::keyboard::NamedKey::Space => Some(' '),
|
||||||
|
winit::keyboard::NamedKey::Enter => Some('\u{000d}'),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
winit::keyboard::Key::Character(str) => {
|
||||||
|
let char = str.chars().next().unwrap_or('\0');
|
||||||
|
(None, Some(char))
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut key_event = KeyEvent {
|
||||||
|
size: size_of::<KeyEvent>(),
|
||||||
|
focus_on_editable_field: 1,
|
||||||
|
modifiers: context.input_state.cef_modifiers(&event.location, event.repeat).raw(),
|
||||||
|
is_system_key: 0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(named_key) = named_key {
|
||||||
|
key_event.native_key_code = named_key.to_dom_bits();
|
||||||
|
key_event.windows_key_code = named_key.to_vk_bits();
|
||||||
|
} else if let Some(char) = character {
|
||||||
|
key_event.native_key_code = char.to_dom_bits();
|
||||||
|
key_event.windows_key_code = char.to_vk_bits();
|
||||||
|
}
|
||||||
|
|
||||||
|
match event.state {
|
||||||
|
ElementState::Pressed => {
|
||||||
|
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_RAWKEYDOWN);
|
||||||
|
host.send_key_event(Some(&key_event));
|
||||||
|
|
||||||
|
if let Some(char) = character {
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
char.encode_utf16(&mut buf);
|
||||||
|
key_event.character = buf[0];
|
||||||
|
let mut buf = [0; 2];
|
||||||
|
char.to_lowercase().next().unwrap().encode_utf16(&mut buf);
|
||||||
|
key_event.unmodified_character = buf[0];
|
||||||
|
|
||||||
|
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_CHAR);
|
||||||
|
host.send_key_event(Some(&key_event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElementState::Released => {
|
||||||
|
key_event.type_ = KeyEventType::from(cef_key_event_type_t::KEYEVENT_KEYUP);
|
||||||
|
host.send_key_event(Some(&key_event));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => return Some(e),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct MouseState {
|
||||||
|
left: bool,
|
||||||
|
right: bool,
|
||||||
|
middle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub(crate) struct MousePosition {
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PhysicalPosition<f64>> for MousePosition {
|
||||||
|
fn from(position: &PhysicalPosition<f64>) -> Self {
|
||||||
|
Self {
|
||||||
|
x: position.x as usize,
|
||||||
|
y: position.y as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct InputState {
|
||||||
|
modifiers: winit::keyboard::ModifiersState,
|
||||||
|
mouse_position: MousePosition,
|
||||||
|
mouse_state: MouseState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
fn update_modifiers(&mut self, modifiers: &winit::keyboard::ModifiersState) {
|
||||||
|
self.modifiers = *modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_mouse_position(&mut self, position: &PhysicalPosition<f64>) {
|
||||||
|
self.mouse_position = position.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_mouse_state(&mut self, state: MouseState) {
|
||||||
|
self.mouse_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cef_modifiers(&self, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> CefModifiers {
|
||||||
|
CefModifiers::new(self, location, is_repeat)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cef_modifiers_mouse_event(&self) -> CefModifiers {
|
||||||
|
self.cef_modifiers(&winit::keyboard::KeyLocation::Standard, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InputState> for CefModifiers {
|
||||||
|
fn from(val: InputState) -> Self {
|
||||||
|
CefModifiers::new(&val, &winit::keyboard::KeyLocation::Standard, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&InputState> for MouseEvent {
|
||||||
|
fn from(val: &InputState) -> Self {
|
||||||
|
MouseEvent {
|
||||||
|
x: val.mouse_position.x as i32,
|
||||||
|
y: val.mouse_position.y as i32,
|
||||||
|
modifiers: val.cef_modifiers_mouse_event().raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CefModifiers(u32);
|
||||||
|
|
||||||
|
impl CefModifiers {
|
||||||
|
fn new(input_state: &InputState, location: &winit::keyboard::KeyLocation, is_repeat: bool) -> Self {
|
||||||
|
let mut inner = 0;
|
||||||
|
|
||||||
|
if input_state.modifiers.shift_key() {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
|
||||||
|
}
|
||||||
|
if input_state.modifiers.control_key() {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
|
||||||
|
}
|
||||||
|
if input_state.modifiers.alt_key() {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
|
||||||
|
}
|
||||||
|
if input_state.modifiers.super_key() {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if input_state.mouse_state.left {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32;
|
||||||
|
}
|
||||||
|
if input_state.mouse_state.right {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32;
|
||||||
|
}
|
||||||
|
if input_state.mouse_state.middle {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_repeat {
|
||||||
|
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner |= match location {
|
||||||
|
winit::keyboard::KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT as u32,
|
||||||
|
winit::keyboard::KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT as u32,
|
||||||
|
winit::keyboard::KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD as u32,
|
||||||
|
winit::keyboard::KeyLocation::Standard => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw(&self) -> u32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
macro_rules! map_enum {
|
||||||
|
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
|
||||||
|
match $target {
|
||||||
|
$(
|
||||||
|
$enum::$variant => $code,
|
||||||
|
)+
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! map {
|
||||||
|
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
|
||||||
|
match $target {
|
||||||
|
$(
|
||||||
|
$variant => $code,
|
||||||
|
)+
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows Virtual keyboard binary representation
|
||||||
|
pub(crate) trait ToVKBits {
|
||||||
|
fn to_vk_bits(&self) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToVKBits for winit::keyboard::NamedKey {
|
||||||
|
fn to_vk_bits(&self) -> i32 {
|
||||||
|
use winit::keyboard::NamedKey;
|
||||||
|
map_enum!(
|
||||||
|
self,
|
||||||
|
NamedKey,
|
||||||
|
(0x12, Alt),
|
||||||
|
(0xA5, AltGraph),
|
||||||
|
(0x14, CapsLock),
|
||||||
|
(0x11, Control),
|
||||||
|
(0x90, NumLock),
|
||||||
|
(0x91, ScrollLock),
|
||||||
|
(0x10, Shift),
|
||||||
|
(0x5B, Meta),
|
||||||
|
(0x5C, Super),
|
||||||
|
(0x0D, Enter),
|
||||||
|
(0x09, Tab),
|
||||||
|
(0x20, Space),
|
||||||
|
(0x28, ArrowDown),
|
||||||
|
(0x25, ArrowLeft),
|
||||||
|
(0x27, ArrowRight),
|
||||||
|
(0x26, ArrowUp),
|
||||||
|
(0x23, End),
|
||||||
|
(0x24, Home),
|
||||||
|
(0x22, PageDown),
|
||||||
|
(0x21, PageUp),
|
||||||
|
(0x08, Backspace),
|
||||||
|
(0x0C, Clear),
|
||||||
|
(0xF7, CrSel),
|
||||||
|
(0x2E, Delete),
|
||||||
|
(0xF9, EraseEof),
|
||||||
|
(0xF8, ExSel),
|
||||||
|
(0x2D, Insert),
|
||||||
|
(0x1E, Accept),
|
||||||
|
(0xF6, Attn),
|
||||||
|
(0x03, Cancel),
|
||||||
|
(0x5D, ContextMenu),
|
||||||
|
(0x1B, Escape),
|
||||||
|
(0x2B, Execute),
|
||||||
|
(0x2F, Help),
|
||||||
|
(0x13, Pause),
|
||||||
|
(0xFA, Play),
|
||||||
|
(0x5D, Props),
|
||||||
|
(0x29, Select),
|
||||||
|
(0xFB, ZoomIn),
|
||||||
|
(0xFB, ZoomOut),
|
||||||
|
(0x2C, PrintScreen),
|
||||||
|
(0x5F, Standby),
|
||||||
|
(0x1C, Convert),
|
||||||
|
(0x18, FinalMode),
|
||||||
|
(0x1F, ModeChange),
|
||||||
|
(0x1D, NonConvert),
|
||||||
|
(0xE5, Process),
|
||||||
|
(0x15, HangulMode),
|
||||||
|
(0x19, HanjaMode),
|
||||||
|
(0x17, JunjaMode),
|
||||||
|
(0x15, KanaMode),
|
||||||
|
(0x19, KanjiMode),
|
||||||
|
(0xB0, MediaFastForward),
|
||||||
|
(0xB3, MediaPause),
|
||||||
|
(0xB3, MediaPlay),
|
||||||
|
(0xB3, MediaPlayPause),
|
||||||
|
(0xB1, MediaRewind),
|
||||||
|
(0xB2, MediaStop),
|
||||||
|
(0xB0, MediaTrackNext),
|
||||||
|
(0xB1, MediaTrackPrevious),
|
||||||
|
(0x2A, Print),
|
||||||
|
(0xAE, AudioVolumeDown),
|
||||||
|
(0xAF, AudioVolumeUp),
|
||||||
|
(0xAD, AudioVolumeMute),
|
||||||
|
(0xB6, LaunchApplication1),
|
||||||
|
(0xB7, LaunchApplication2),
|
||||||
|
(0xB4, LaunchMail),
|
||||||
|
(0xB5, LaunchMediaPlayer),
|
||||||
|
(0xB5, LaunchMusicPlayer),
|
||||||
|
(0xA6, BrowserBack),
|
||||||
|
(0xAB, BrowserFavorites),
|
||||||
|
(0xA7, BrowserForward),
|
||||||
|
(0xAC, BrowserHome),
|
||||||
|
(0xA8, BrowserRefresh),
|
||||||
|
(0xAA, BrowserSearch),
|
||||||
|
(0xA9, BrowserStop),
|
||||||
|
(0xFB, ZoomToggle),
|
||||||
|
(0x70, F1),
|
||||||
|
(0x71, F2),
|
||||||
|
(0x72, F3),
|
||||||
|
(0x73, F4),
|
||||||
|
(0x74, F5),
|
||||||
|
(0x75, F6),
|
||||||
|
(0x76, F7),
|
||||||
|
(0x77, F8),
|
||||||
|
(0x78, F9),
|
||||||
|
(0x79, F10),
|
||||||
|
(0x7A, F11),
|
||||||
|
(0x7B, F12),
|
||||||
|
(0x7C, F13),
|
||||||
|
(0x7D, F14),
|
||||||
|
(0x7E, F15),
|
||||||
|
(0x7F, F16),
|
||||||
|
(0x80, F17),
|
||||||
|
(0x81, F18),
|
||||||
|
(0x82, F19),
|
||||||
|
(0x83, F20),
|
||||||
|
(0x84, F21),
|
||||||
|
(0x85, F22),
|
||||||
|
(0x86, F23),
|
||||||
|
(0x87, F24),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToVKBits for char {
|
||||||
|
fn to_vk_bits(&self) -> i32 {
|
||||||
|
map!(
|
||||||
|
self,
|
||||||
|
(0x0041, 'a'),
|
||||||
|
(0x0042, 'b'),
|
||||||
|
(0x0043, 'c'),
|
||||||
|
(0x0044, 'd'),
|
||||||
|
(0x0045, 'e'),
|
||||||
|
(0x0046, 'f'),
|
||||||
|
(0x0047, 'g'),
|
||||||
|
(0x0048, 'h'),
|
||||||
|
(0x0049, 'i'),
|
||||||
|
(0x004a, 'j'),
|
||||||
|
(0x004b, 'k'),
|
||||||
|
(0x004c, 'l'),
|
||||||
|
(0x004d, 'm'),
|
||||||
|
(0x004e, 'n'),
|
||||||
|
(0x004f, 'o'),
|
||||||
|
(0x0050, 'p'),
|
||||||
|
(0x0051, 'q'),
|
||||||
|
(0x0052, 'r'),
|
||||||
|
(0x0053, 's'),
|
||||||
|
(0x0054, 't'),
|
||||||
|
(0x0055, 'u'),
|
||||||
|
(0x0056, 'v'),
|
||||||
|
(0x0057, 'w'),
|
||||||
|
(0x0058, 'x'),
|
||||||
|
(0x0059, 'y'),
|
||||||
|
(0x005a, 'z'),
|
||||||
|
(0x0041, 'A'),
|
||||||
|
(0x0042, 'B'),
|
||||||
|
(0x0043, 'C'),
|
||||||
|
(0x0044, 'D'),
|
||||||
|
(0x0045, 'E'),
|
||||||
|
(0x0046, 'F'),
|
||||||
|
(0x0047, 'G'),
|
||||||
|
(0x0048, 'H'),
|
||||||
|
(0x0049, 'I'),
|
||||||
|
(0x004a, 'J'),
|
||||||
|
(0x004b, 'K'),
|
||||||
|
(0x004c, 'L'),
|
||||||
|
(0x004d, 'M'),
|
||||||
|
(0x004e, 'N'),
|
||||||
|
(0x004f, 'O'),
|
||||||
|
(0x0050, 'P'),
|
||||||
|
(0x0051, 'Q'),
|
||||||
|
(0x0052, 'R'),
|
||||||
|
(0x0053, 'S'),
|
||||||
|
(0x0054, 'T'),
|
||||||
|
(0x0055, 'U'),
|
||||||
|
(0x0056, 'V'),
|
||||||
|
(0x0057, 'W'),
|
||||||
|
(0x0058, 'X'),
|
||||||
|
(0x0059, 'Y'),
|
||||||
|
(0x005a, 'Z'),
|
||||||
|
(0x0031, '1'),
|
||||||
|
(0x0032, '2'),
|
||||||
|
(0x0032, '3'),
|
||||||
|
(0x0033, '4'),
|
||||||
|
(0x0034, '5'),
|
||||||
|
(0x0035, '6'),
|
||||||
|
(0x0036, '7'),
|
||||||
|
(0x0037, '8'),
|
||||||
|
(0x0039, '9'),
|
||||||
|
(0x0030, '0'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chromium dom key binary representation
|
||||||
|
pub(crate) trait ToDomBits {
|
||||||
|
fn to_dom_bits(&self) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToDomBits for winit::keyboard::NamedKey {
|
||||||
|
fn to_dom_bits(&self) -> i32 {
|
||||||
|
use winit::keyboard::NamedKey;
|
||||||
|
map_enum!(
|
||||||
|
self,
|
||||||
|
NamedKey,
|
||||||
|
(0x0000, Hyper),
|
||||||
|
(0x0085, Super),
|
||||||
|
(0x0025, Control),
|
||||||
|
(0x0032, Shift),
|
||||||
|
(0x0040, Alt),
|
||||||
|
(0x0000, Fn),
|
||||||
|
(0x0000, FnLock),
|
||||||
|
(0x0024, Enter),
|
||||||
|
(0x0009, Escape),
|
||||||
|
(0x0016, Backspace),
|
||||||
|
(0x0017, Tab),
|
||||||
|
(0x0041, Space),
|
||||||
|
(0x0042, CapsLock),
|
||||||
|
(0x0043, F1),
|
||||||
|
(0x0044, F2),
|
||||||
|
(0x0045, F3),
|
||||||
|
(0x0046, F4),
|
||||||
|
(0x0047, F5),
|
||||||
|
(0x0048, F6),
|
||||||
|
(0x0049, F7),
|
||||||
|
(0x004a, F8),
|
||||||
|
(0x004b, F9),
|
||||||
|
(0x004c, F10),
|
||||||
|
(0x005f, F11),
|
||||||
|
(0x0060, F12),
|
||||||
|
(0x006b, PrintScreen),
|
||||||
|
(0x004e, ScrollLock),
|
||||||
|
(0x007f, Pause),
|
||||||
|
(0x0076, Insert),
|
||||||
|
(0x006e, Home),
|
||||||
|
(0x0070, PageUp),
|
||||||
|
(0x0077, Delete),
|
||||||
|
(0x0073, End),
|
||||||
|
(0x0075, PageDown),
|
||||||
|
(0x0072, ArrowRight),
|
||||||
|
(0x0071, ArrowLeft),
|
||||||
|
(0x0074, ArrowDown),
|
||||||
|
(0x006f, ArrowUp),
|
||||||
|
(0x004d, NumLock),
|
||||||
|
(0x0087, ContextMenu),
|
||||||
|
(0x007c, Power),
|
||||||
|
(0x00bf, F13),
|
||||||
|
(0x00c0, F14),
|
||||||
|
(0x00c1, F15),
|
||||||
|
(0x00c2, F16),
|
||||||
|
(0x00c3, F17),
|
||||||
|
(0x00c4, F18),
|
||||||
|
(0x00c5, F19),
|
||||||
|
(0x00c6, F20),
|
||||||
|
(0x00c7, F21),
|
||||||
|
(0x00c8, F22),
|
||||||
|
(0x00c9, F23),
|
||||||
|
(0x00ca, F24),
|
||||||
|
(0x008e, Open),
|
||||||
|
(0x0092, Help),
|
||||||
|
(0x008c, Select),
|
||||||
|
(0x0089, Again),
|
||||||
|
(0x008b, Undo),
|
||||||
|
(0x0091, Cut),
|
||||||
|
(0x008d, Copy),
|
||||||
|
(0x008f, Paste),
|
||||||
|
(0x0090, Find),
|
||||||
|
(0x0079, AudioVolumeMute),
|
||||||
|
(0x007b, AudioVolumeUp),
|
||||||
|
(0x007a, AudioVolumeDown),
|
||||||
|
(0x0065, KanaMode),
|
||||||
|
(0x0064, Convert),
|
||||||
|
(0x0066, NonConvert),
|
||||||
|
(0x0000, Props),
|
||||||
|
(0x00e9, BrightnessUp),
|
||||||
|
(0x00e8, BrightnessDown),
|
||||||
|
(0x00d7, MediaPlay),
|
||||||
|
(0x00d1, MediaPause),
|
||||||
|
(0x00af, MediaRecord),
|
||||||
|
(0x00d8, MediaFastForward),
|
||||||
|
(0x00b0, MediaRewind),
|
||||||
|
(0x00ab, MediaTrackNext),
|
||||||
|
(0x00ad, MediaTrackPrevious),
|
||||||
|
(0x00ae, MediaStop),
|
||||||
|
(0x00a9, Eject),
|
||||||
|
(0x00ac, MediaPlayPause),
|
||||||
|
(0x00a3, LaunchMail),
|
||||||
|
(0x024d, LaunchScreenSaver),
|
||||||
|
(0x00e1, BrowserSearch),
|
||||||
|
(0x00b4, BrowserHome),
|
||||||
|
(0x00a6, BrowserBack),
|
||||||
|
(0x00a7, BrowserForward),
|
||||||
|
(0x0088, BrowserStop),
|
||||||
|
(0x00b5, BrowserRefresh),
|
||||||
|
(0x00a4, BrowserFavorites),
|
||||||
|
(0x017c, ZoomToggle),
|
||||||
|
(0x00f0, MailReply),
|
||||||
|
(0x00f1, MailForward),
|
||||||
|
(0x00ef, MailSend),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToDomBits for char {
|
||||||
|
fn to_dom_bits(&self) -> i32 {
|
||||||
|
map!(
|
||||||
|
self,
|
||||||
|
(0x0026, 'a'),
|
||||||
|
(0x0038, 'b'),
|
||||||
|
(0x0036, 'c'),
|
||||||
|
(0x0028, 'd'),
|
||||||
|
(0x001a, 'e'),
|
||||||
|
(0x0029, 'f'),
|
||||||
|
(0x002a, 'g'),
|
||||||
|
(0x002b, 'h'),
|
||||||
|
(0x001f, 'i'),
|
||||||
|
(0x002c, 'j'),
|
||||||
|
(0x002d, 'k'),
|
||||||
|
(0x002e, 'l'),
|
||||||
|
(0x003a, 'm'),
|
||||||
|
(0x0039, 'n'),
|
||||||
|
(0x0020, 'o'),
|
||||||
|
(0x0021, 'p'),
|
||||||
|
(0x0018, 'q'),
|
||||||
|
(0x001b, 'r'),
|
||||||
|
(0x0027, 's'),
|
||||||
|
(0x001c, 't'),
|
||||||
|
(0x001e, 'u'),
|
||||||
|
(0x0037, 'v'),
|
||||||
|
(0x0019, 'w'),
|
||||||
|
(0x0035, 'x'),
|
||||||
|
(0x001d, 'y'),
|
||||||
|
(0x0034, 'z'),
|
||||||
|
(0x0026, 'A'),
|
||||||
|
(0x0038, 'B'),
|
||||||
|
(0x0036, 'C'),
|
||||||
|
(0x0028, 'D'),
|
||||||
|
(0x001a, 'E'),
|
||||||
|
(0x0029, 'F'),
|
||||||
|
(0x002a, 'G'),
|
||||||
|
(0x002b, 'H'),
|
||||||
|
(0x001f, 'I'),
|
||||||
|
(0x002c, 'J'),
|
||||||
|
(0x002d, 'K'),
|
||||||
|
(0x002e, 'L'),
|
||||||
|
(0x003a, 'M'),
|
||||||
|
(0x0039, 'N'),
|
||||||
|
(0x0020, 'O'),
|
||||||
|
(0x0021, 'P'),
|
||||||
|
(0x0018, 'Q'),
|
||||||
|
(0x001b, 'R'),
|
||||||
|
(0x0027, 'S'),
|
||||||
|
(0x001c, 'T'),
|
||||||
|
(0x001e, 'U'),
|
||||||
|
(0x0037, 'V'),
|
||||||
|
(0x0019, 'W'),
|
||||||
|
(0x0035, 'X'),
|
||||||
|
(0x001d, 'Y'),
|
||||||
|
(0x0034, 'Z'),
|
||||||
|
(0x000a, '1'),
|
||||||
|
(0x000b, '2'),
|
||||||
|
(0x000c, '3'),
|
||||||
|
(0x000d, '4'),
|
||||||
|
(0x000e, '5'),
|
||||||
|
(0x000f, '6'),
|
||||||
|
(0x0010, '7'),
|
||||||
|
(0x0011, '8'),
|
||||||
|
(0x0012, '9'),
|
||||||
|
(0x0013, '0'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
mod app;
|
||||||
|
mod browser_process_handler;
|
||||||
|
mod client;
|
||||||
|
mod non_browser_app;
|
||||||
|
mod render_handler;
|
||||||
|
|
||||||
|
pub(crate) use app::AppImpl;
|
||||||
|
pub(crate) use client::ClientImpl;
|
||||||
|
pub(crate) use non_browser_app::NonBrowserAppImpl;
|
||||||
|
pub(crate) use render_handler::RenderHandlerImpl;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
|
||||||
|
|
||||||
|
use crate::cef::CefEventHandler;
|
||||||
|
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||||
|
|
||||||
|
use super::browser_process_handler::BrowserProcessHandlerImpl;
|
||||||
|
|
||||||
|
pub(crate) struct AppImpl<H: CefEventHandler> {
|
||||||
|
object: *mut RcImpl<_cef_app_t, Self>,
|
||||||
|
event_handler: H,
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> AppImpl<H> {
|
||||||
|
pub(crate) fn new(event_handler: H) -> Self {
|
||||||
|
Self {
|
||||||
|
object: std::ptr::null_mut(),
|
||||||
|
event_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: CefEventHandler> ImplApp for AppImpl<H> {
|
||||||
|
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||||
|
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||||
|
GraphiteSchemeHandlerFactory::register_schemes(registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_app_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: CefEventHandler> Clone for AppImpl<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 AppImpl<H> {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> WrapApp for AppImpl<H> {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
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 crate::cef::CefEventHandler;
|
||||||
|
use crate::cef::scheme_handler::{GRAPHITE_SCHEME, GraphiteSchemeHandlerFactory};
|
||||||
|
|
||||||
|
pub(crate) struct BrowserProcessHandlerImpl<H: CefEventHandler> {
|
||||||
|
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||||
|
event_handler: H,
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
|
||||||
|
pub(crate) fn new(event_handler: H) -> Self {
|
||||||
|
Self {
|
||||||
|
object: std::ptr::null_mut(),
|
||||||
|
event_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: CefEventHandler> 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())));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_browser_process_handler_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
|
||||||
|
self.event_handler.schedule_cef_message_loop_work(Instant::now() + Duration::from_millis(delay_ms as u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: CefEventHandler> Clone for BrowserProcessHandlerImpl<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 BrowserProcessHandlerImpl<H> {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{ImplClient, RenderHandler, WrapClient};
|
||||||
|
|
||||||
|
pub(crate) struct ClientImpl {
|
||||||
|
object: *mut RcImpl<_cef_client_t, Self>,
|
||||||
|
render_handler: RenderHandler,
|
||||||
|
}
|
||||||
|
impl ClientImpl {
|
||||||
|
pub(crate) fn new(render_handler: RenderHandler) -> Self {
|
||||||
|
Self {
|
||||||
|
object: std::ptr::null_mut(),
|
||||||
|
render_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImplClient for ClientImpl {
|
||||||
|
fn render_handler(&self) -> Option<RenderHandler> {
|
||||||
|
Some(self.render_handler.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_client_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ClientImpl {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
unsafe {
|
||||||
|
let rc_impl = &mut *self.object;
|
||||||
|
rc_impl.interface.add_ref();
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
object: self.object,
|
||||||
|
render_handler: self.render_handler.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rc for ClientImpl {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WrapClient for ClientImpl {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
|
||||||
|
|
||||||
|
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||||
|
|
||||||
|
pub(crate) struct NonBrowserAppImpl {
|
||||||
|
object: *mut RcImpl<_cef_app_t, Self>,
|
||||||
|
}
|
||||||
|
impl NonBrowserAppImpl {
|
||||||
|
pub(crate) fn app() -> App {
|
||||||
|
App::new(Self { object: std::ptr::null_mut() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImplApp for NonBrowserAppImpl {
|
||||||
|
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||||
|
GraphiteSchemeHandlerFactory::register_schemes(registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_app_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for NonBrowserAppImpl {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
unsafe {
|
||||||
|
let rc_impl = &mut *self.object;
|
||||||
|
rc_impl.interface.add_ref();
|
||||||
|
}
|
||||||
|
Self { object: self.object }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Rc for NonBrowserAppImpl {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WrapApp for NonBrowserAppImpl {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use cef::rc::{Rc, RcImpl};
|
||||||
|
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
|
||||||
|
use cef::{Browser, ImplBrowser, ImplBrowserHost, ImplRenderHandler, PaintElementType, Rect, WrapRenderHandler};
|
||||||
|
|
||||||
|
use crate::FrameBuffer;
|
||||||
|
use crate::cef::CefEventHandler;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
object: std::ptr::null_mut(),
|
||||||
|
event_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||||
|
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
|
||||||
|
if let Some(rect) = rect {
|
||||||
|
let view = self.event_handler.window_size();
|
||||||
|
*rect = Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: view.width as i32,
|
||||||
|
height: view.height as i32,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_paint(
|
||||||
|
&self,
|
||||||
|
browser: Option<&mut Browser>,
|
||||||
|
_type_: PaintElementType,
|
||||||
|
_dirty_rect_count: usize,
|
||||||
|
_dirty_rects: Option<&Rect>,
|
||||||
|
buffer: *const u8,
|
||||||
|
width: ::std::os::raw::c_int,
|
||||||
|
height: ::std::os::raw::c_int,
|
||||||
|
) {
|
||||||
|
let buffer_size = (width * height * 4) as usize;
|
||||||
|
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
|
||||||
|
let frame_buffer = FrameBuffer::new(buffer_slice.to_vec(), width as usize, height as usize).expect("Failed to create frame buffer");
|
||||||
|
|
||||||
|
let draw_successful = self.event_handler.draw(frame_buffer);
|
||||||
|
if !draw_successful {
|
||||||
|
if let Some(browser) = browser {
|
||||||
|
browser.host().unwrap().was_resized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_raw(&self) -> *mut _cef_render_handler_t {
|
||||||
|
self.object.cast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: CefEventHandler> Clone for RenderHandlerImpl<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 RenderHandlerImpl<H> {
|
||||||
|
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||||
|
unsafe {
|
||||||
|
let base = &*self.object;
|
||||||
|
std::mem::transmute(&base.cef_object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H: CefEventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
|
||||||
|
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
|
||||||
|
self.object = object;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
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};
|
||||||
|
|
||||||
|
pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static";
|
||||||
|
pub(crate) const FRONTEND_DOMAIN: &str = "frontend";
|
||||||
|
|
||||||
|
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()),
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use winit::event_loop::{EventLoop, EventLoopProxy};
|
||||||
|
|
||||||
|
mod cef;
|
||||||
|
use cef::Setup;
|
||||||
|
|
||||||
|
mod render;
|
||||||
|
use render::{FrameBuffer, GraphicsState};
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
use app::WinitApp;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum CustomEvent {
|
||||||
|
UiUpdate,
|
||||||
|
ScheduleBrowserWork(Instant),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WindowState {
|
||||||
|
width: Option<usize>,
|
||||||
|
height: Option<usize>,
|
||||||
|
ui_frame_buffer: Option<FrameBuffer>,
|
||||||
|
_viewport_frame_buffer: Option<FrameBuffer>,
|
||||||
|
graphics_state: Option<GraphicsState>,
|
||||||
|
event_loop_proxy: Option<EventLoopProxy<CustomEvent>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
ui_frame_buffer: None,
|
||||||
|
_viewport_frame_buffer: None,
|
||||||
|
graphics_state: None,
|
||||||
|
event_loop_proxy: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(self) -> WindowStateHandle {
|
||||||
|
WindowStateHandle { inner: Arc::new(Mutex::new(self)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WindowStateHandle {
|
||||||
|
inner: Arc<Mutex<WindowState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowStateHandle {
|
||||||
|
fn with<'a, P>(&self, p: P) -> Result<(), PoisonError<MutexGuard<'a, WindowState>>>
|
||||||
|
where
|
||||||
|
P: FnOnce(&mut WindowState),
|
||||||
|
{
|
||||||
|
match self.inner.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
p(&mut guard);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(_) => todo!("not error handling yet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for WindowStateHandle {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self { inner: self.inner.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CefHandler {
|
||||||
|
window_state: WindowStateHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CefHandler {
|
||||||
|
fn new(window_state: WindowStateHandle) -> Self {
|
||||||
|
Self { window_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cef::CefEventHandler for CefHandler {
|
||||||
|
fn window_size(&self) -> cef::WindowSize {
|
||||||
|
let mut w = 1;
|
||||||
|
let mut h = 1;
|
||||||
|
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
if let WindowState {
|
||||||
|
width: Some(width),
|
||||||
|
height: Some(height),
|
||||||
|
..
|
||||||
|
} = s
|
||||||
|
{
|
||||||
|
w = *width;
|
||||||
|
h = *height;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cef::WindowSize::new(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame_buffer: FrameBuffer) -> bool {
|
||||||
|
let mut correct_size = true;
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
if let Some(event_loop_proxy) = &s.event_loop_proxy {
|
||||||
|
let _ = event_loop_proxy.send_event(CustomEvent::UiUpdate);
|
||||||
|
}
|
||||||
|
if frame_buffer.width() != s.width.unwrap_or(1) || frame_buffer.height() != s.height.unwrap_or(1) {
|
||||||
|
correct_size = false;
|
||||||
|
} else {
|
||||||
|
s.ui_frame_buffer = Some(frame_buffer);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
correct_size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
|
||||||
|
self.window_state
|
||||||
|
.with(|s| {
|
||||||
|
let Some(event_loop_proxy) = &mut s.event_loop_proxy else { return };
|
||||||
|
let _ = event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
|
||||||
|
|
||||||
|
let cef_context = match cef::Context::<Setup>::new() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(cef::SetupError::Subprocess) => exit(0),
|
||||||
|
Err(cef::SetupError::SubprocessFailed(t)) => {
|
||||||
|
tracing::error!("Subprocess of type {t} failed");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_state = WindowState::new().handle();
|
||||||
|
|
||||||
|
window_state
|
||||||
|
.with(|s| {
|
||||||
|
s.width = Some(1200);
|
||||||
|
s.height = Some(800);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
|
||||||
|
|
||||||
|
window_state.with(|s| s.event_loop_proxy = Some(event_loop.create_proxy())).unwrap();
|
||||||
|
|
||||||
|
let cef_context = match cef_context.init(CefHandler::new(window_state.clone())) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(cef::InitError::InitializationFailed) => {
|
||||||
|
tracing::error!("Cef initialization failed");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Cef initialized successfully");
|
||||||
|
|
||||||
|
let mut winit_app = WinitApp::new(window_state, cef_context);
|
||||||
|
|
||||||
|
event_loop.run_app(&mut winit_app).unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
pub(crate) struct FrameBuffer {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
impl std::fmt::Debug for FrameBuffer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("WindowState")
|
||||||
|
.field("width", &self.width)
|
||||||
|
.field("height", &self.height)
|
||||||
|
.field("len", &self.buffer.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum FrameBufferError {
|
||||||
|
#[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")]
|
||||||
|
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameBuffer {
|
||||||
|
pub(crate) fn new(buffer: Vec<u8>, width: usize, height: usize) -> Result<Self, FrameBufferError> {
|
||||||
|
let fb = Self { buffer, width, height };
|
||||||
|
fb.validate_size()?;
|
||||||
|
Ok(fb)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn buffer(&self) -> &[u8] {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn width(&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn height(&self) -> usize {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_size(&self) -> Result<(), FrameBufferError> {
|
||||||
|
if self.buffer.len() != self.width * self.height * 4 {
|
||||||
|
Err(FrameBufferError::InvalidSize {
|
||||||
|
buffer_size: self.buffer.len(),
|
||||||
|
expected_size: self.width * self.height * 4,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct GraphicsState {
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
texture: Option<wgpu::Texture>,
|
||||||
|
bind_group: Option<wgpu::BindGroup>,
|
||||||
|
render_pipeline: wgpu::RenderPipeline,
|
||||||
|
sampler: wgpu::Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphicsState {
|
||||||
|
pub(crate) async fn new(window: Arc<Window>) -> Self {
|
||||||
|
let size = window.inner_size();
|
||||||
|
|
||||||
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::PRIMARY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let surface = instance.create_surface(window).unwrap();
|
||||||
|
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
label: None,
|
||||||
|
memory_hints: Default::default(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
|
||||||
|
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
present_mode: surface_caps.present_modes[0],
|
||||||
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
// Create shader module
|
||||||
|
let shader = device.create_shader_module(wgpu::include_wgsl!("render/fullscreen_texture.wgsl"));
|
||||||
|
|
||||||
|
// Create sampler
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
multisampled: false,
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("texture_bind_group_layout"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Render Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[&texture_bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Render Pipeline"),
|
||||||
|
layout: Some(&render_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: config.format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: Some(wgpu::Face::Back),
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
unclipped_depth: false,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 1,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut graphics_state = Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
texture: None,
|
||||||
|
bind_group: None,
|
||||||
|
render_pipeline,
|
||||||
|
sampler,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize with a test pattern so we always have something to render
|
||||||
|
let width = 800;
|
||||||
|
let height = 600;
|
||||||
|
let initial_data = vec![34u8; width * height * 4]; // Gray texture #222222FF
|
||||||
|
|
||||||
|
let fb = FrameBuffer::new(initial_data, width, height)
|
||||||
|
.map_err(|e| {
|
||||||
|
panic!("Failed to create initial FrameBuffer: {}", e);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
graphics_state.update_texture(&fb);
|
||||||
|
|
||||||
|
graphics_state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resize(&mut self, width: usize, height: usize) {
|
||||||
|
if width > 0 && height > 0 && (self.config.width != width as u32 || self.config.height != height as u32) {
|
||||||
|
self.config.width = width as u32;
|
||||||
|
self.config.height = height as u32;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_texture(&mut self, frame_buffer: &FrameBuffer) {
|
||||||
|
let data = frame_buffer.buffer();
|
||||||
|
let width = frame_buffer.width() as u32;
|
||||||
|
let height = frame_buffer.height() as u32;
|
||||||
|
|
||||||
|
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
|
||||||
|
self.config.width = width;
|
||||||
|
self.config.height = height;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("CEF Texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Bgra8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
self.queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(4 * width),
|
||||||
|
rows_per_image: Some(height),
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
layout: &self.render_pipeline.get_bind_group_layout(0),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some("texture_bind_group"),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.texture = Some(texture);
|
||||||
|
self.bind_group = Some(bind_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||||
|
let output = self.surface.get_current_texture()?;
|
||||||
|
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("Render Pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
render_pass.set_pipeline(&self.render_pipeline);
|
||||||
|
if let Some(bind_group) = &self.bind_group {
|
||||||
|
render_pass.set_bind_group(0, bind_group, &[]);
|
||||||
|
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
|
||||||
|
} else {
|
||||||
|
tracing::warn!("No bind group available - showing clear color only");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) tex_coords: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
let pos = array(
|
||||||
|
// 1st triangle
|
||||||
|
vec2f( -1.0, -1.0), // center
|
||||||
|
vec2f( 1.0, -1.0), // right, center
|
||||||
|
vec2f( -1.0, 1.0), // center, top
|
||||||
|
|
||||||
|
// 2nd triangle
|
||||||
|
vec2f( -1.0, 1.0), // center, top
|
||||||
|
vec2f( 1.0, -1.0), // right, center
|
||||||
|
vec2f( 1.0, 1.0), // right, top
|
||||||
|
);
|
||||||
|
let xy = pos[vertex_index];
|
||||||
|
out.clip_position = vec4f(xy , 0.0, 1.0);
|
||||||
|
let coords = (xy / 2. + 0.5);
|
||||||
|
out.tex_coords = vec2f(coords.x, 1. - coords.y);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var t_diffuse: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var s_diffuse: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "graphite-editor"
|
name = "graphite-editor"
|
||||||
publish = false
|
publish = false
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.88"
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ pub enum InputMapperMessage {
|
|||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
PointerMove,
|
PointerMove,
|
||||||
|
PointerShake,
|
||||||
WheelScroll,
|
WheelScroll,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,15 @@ pub fn input_mappings() -> Mapping {
|
|||||||
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
|
entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop),
|
||||||
//
|
//
|
||||||
// NodeGraphMessage
|
// NodeGraphMessage
|
||||||
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: false}),
|
entry!(KeyDown(MouseLeft); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: false }),
|
||||||
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: false, alt_click: false, right_click: false}),
|
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: false, alt_click: false, right_click: false }),
|
||||||
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: true, alt_click: false, right_click: false}),
|
entry!(KeyDown(MouseLeft); modifiers=[Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: true, alt_click: false, right_click: false }),
|
||||||
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown {shift_click: true, control_click: true, alt_click: false, right_click: false}),
|
entry!(KeyDown(MouseLeft); modifiers=[Shift, Accel], action_dispatch=NodeGraphMessage::PointerDown { shift_click: true, control_click: true, alt_click: false, right_click: false }),
|
||||||
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: true, right_click: false}),
|
entry!(KeyDown(MouseLeft); modifiers=[Alt], action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: true, right_click: false }),
|
||||||
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown {shift_click: false, control_click: false, alt_click: false, right_click: true}),
|
entry!(KeyDown(MouseRight); action_dispatch=NodeGraphMessage::PointerDown { shift_click: false, control_click: false, alt_click: false, right_click: true }),
|
||||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
|
entry!(DoubleClick(MouseButton::Left); action_dispatch=NodeGraphMessage::EnterNestedNetwork),
|
||||||
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove {shift: Shift}),
|
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=NodeGraphMessage::PointerMove { shift: Shift }),
|
||||||
|
entry!(PointerShake; action_dispatch=NodeGraphMessage::ShakeNode),
|
||||||
entry!(KeyUp(MouseLeft); action_dispatch=NodeGraphMessage::PointerUp),
|
entry!(KeyUp(MouseLeft); action_dispatch=NodeGraphMessage::PointerUp),
|
||||||
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
||||||
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=NodeGraphMessage::DeleteSelectedNodes { delete_children: false }),
|
||||||
@@ -417,7 +418,7 @@ pub fn input_mappings() -> Mapping {
|
|||||||
entry!(KeyDown(Tab); modifiers=[Control], action_dispatch=PortfolioMessage::NextDocument),
|
entry!(KeyDown(Tab); modifiers=[Control], action_dispatch=PortfolioMessage::NextDocument),
|
||||||
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
|
entry!(KeyDown(Tab); modifiers=[Control, Shift], action_dispatch=PortfolioMessage::PrevDocument),
|
||||||
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
|
entry!(KeyDown(KeyW); modifiers=[Accel], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
|
||||||
entry!(KeyDown(KeyW); modifiers=[Accel,Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
entry!(KeyDown(KeyW); modifiers=[Accel, Alt], action_dispatch=PortfolioMessage::CloseAllDocumentsWithConfirmation),
|
||||||
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
|
entry!(KeyDown(KeyO); modifiers=[Accel], action_dispatch=PortfolioMessage::OpenDocument),
|
||||||
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
|
entry!(KeyDown(KeyI); modifiers=[Accel], action_dispatch=PortfolioMessage::Import),
|
||||||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
|
||||||
@@ -440,7 +441,7 @@ pub fn input_mappings() -> Mapping {
|
|||||||
entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview),
|
entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview),
|
||||||
entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation),
|
entry!(KeyDown(Home); modifiers=[Shift], action_dispatch=AnimationMessage::RestartAnimation),
|
||||||
];
|
];
|
||||||
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;
|
let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move, mut pointer_shake) = mappings;
|
||||||
|
|
||||||
let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|a, b| b.modifiers.count_ones().cmp(&a.modifiers.count_ones()));
|
let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|a, b| b.modifiers.count_ones().cmp(&a.modifiers.count_ones()));
|
||||||
// Sort the sublists of `key_up`, `key_down`, `key_up_no_repeat`, and `key_down_no_repeat`
|
// Sort the sublists of `key_up`, `key_down`, `key_up_no_repeat`, and `key_down_no_repeat`
|
||||||
@@ -457,6 +458,8 @@ pub fn input_mappings() -> Mapping {
|
|||||||
sort(&mut wheel_scroll);
|
sort(&mut wheel_scroll);
|
||||||
// Sort `pointer_move`
|
// Sort `pointer_move`
|
||||||
sort(&mut pointer_move);
|
sort(&mut pointer_move);
|
||||||
|
// Sort `pointer_shake`
|
||||||
|
sort(&mut pointer_shake);
|
||||||
|
|
||||||
Mapping {
|
Mapping {
|
||||||
key_up,
|
key_up,
|
||||||
@@ -466,6 +469,7 @@ pub fn input_mappings() -> Mapping {
|
|||||||
double_click,
|
double_click,
|
||||||
wheel_scroll,
|
wheel_scroll,
|
||||||
pointer_move,
|
pointer_move,
|
||||||
|
pointer_shake,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ macro_rules! mapping {
|
|||||||
let mut double_click = KeyMappingEntries::mouse_buttons_arrays();
|
let mut double_click = KeyMappingEntries::mouse_buttons_arrays();
|
||||||
let mut wheel_scroll = KeyMappingEntries::new();
|
let mut wheel_scroll = KeyMappingEntries::new();
|
||||||
let mut pointer_move = KeyMappingEntries::new();
|
let mut pointer_move = KeyMappingEntries::new();
|
||||||
|
let mut pointer_shake = KeyMappingEntries::new();
|
||||||
|
|
||||||
$(
|
$(
|
||||||
// Each of the many entry slices, one specified per action
|
// Each of the many entry slices, one specified per action
|
||||||
@@ -104,6 +105,7 @@ macro_rules! mapping {
|
|||||||
InputMapperMessage::DoubleClick(key) => &mut double_click[key as usize],
|
InputMapperMessage::DoubleClick(key) => &mut double_click[key as usize],
|
||||||
InputMapperMessage::WheelScroll => &mut wheel_scroll,
|
InputMapperMessage::WheelScroll => &mut wheel_scroll,
|
||||||
InputMapperMessage::PointerMove => &mut pointer_move,
|
InputMapperMessage::PointerMove => &mut pointer_move,
|
||||||
|
InputMapperMessage::PointerShake => &mut pointer_shake,
|
||||||
};
|
};
|
||||||
// Push each entry to the corresponding `KeyMappingEntries` list for its input type
|
// Push each entry to the corresponding `KeyMappingEntries` list for its input type
|
||||||
corresponding_list.push(entry.clone());
|
corresponding_list.push(entry.clone());
|
||||||
@@ -111,7 +113,7 @@ macro_rules! mapping {
|
|||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
||||||
(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move)
|
(key_up, key_down, key_up_no_repeat, key_down_no_repeat, double_click, wheel_scroll, pointer_move, pointer_shake)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub struct Mapping {
|
|||||||
pub double_click: [KeyMappingEntries; NUMBER_OF_MOUSE_BUTTONS],
|
pub double_click: [KeyMappingEntries; NUMBER_OF_MOUSE_BUTTONS],
|
||||||
pub wheel_scroll: KeyMappingEntries,
|
pub wheel_scroll: KeyMappingEntries,
|
||||||
pub pointer_move: KeyMappingEntries,
|
pub pointer_move: KeyMappingEntries,
|
||||||
|
pub pointer_shake: KeyMappingEntries,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Mapping {
|
impl Default for Mapping {
|
||||||
@@ -47,6 +48,7 @@ impl Mapping {
|
|||||||
InputMapperMessage::DoubleClick(key) => &self.double_click[*key as usize],
|
InputMapperMessage::DoubleClick(key) => &self.double_click[*key as usize],
|
||||||
InputMapperMessage::WheelScroll => &self.wheel_scroll,
|
InputMapperMessage::WheelScroll => &self.wheel_scroll,
|
||||||
InputMapperMessage::PointerMove => &self.pointer_move,
|
InputMapperMessage::PointerMove => &self.pointer_move,
|
||||||
|
InputMapperMessage::PointerShake => &self.pointer_shake,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ impl Mapping {
|
|||||||
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
|
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
|
||||||
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
|
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
|
||||||
InputMapperMessage::PointerMove => &mut self.pointer_move,
|
InputMapperMessage::PointerMove => &mut self.pointer_move,
|
||||||
|
InputMapperMessage::PointerShake => &mut self.pointer_shake,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub enum InputPreprocessorMessage {
|
|||||||
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
|
PointerShake { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
CurrentTime { timestamp: u64 },
|
CurrentTime { timestamp: u64 },
|
||||||
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
|
|||||||
|
|
||||||
self.translate_mouse_event(mouse_state, false, responses);
|
self.translate_mouse_event(mouse_state, false, responses);
|
||||||
}
|
}
|
||||||
|
InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys } => {
|
||||||
|
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
|
||||||
|
|
||||||
|
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
||||||
|
self.mouse.position = mouse_state.position;
|
||||||
|
|
||||||
|
responses.add(InputMapperMessage::PointerShake);
|
||||||
|
}
|
||||||
InputPreprocessorMessage::CurrentTime { timestamp } => {
|
InputPreprocessorMessage::CurrentTime { timestamp } => {
|
||||||
responses.add(AnimationMessage::SetTime { time: timestamp as f64 });
|
responses.add(AnimationMessage::SetTime { time: timestamp as f64 });
|
||||||
self.time = timestamp;
|
self.time = timestamp;
|
||||||
|
|||||||
@@ -332,6 +332,9 @@ pub enum NumberInputMode {
|
|||||||
pub struct NodeCatalog {
|
pub struct NodeCatalog {
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
|
||||||
|
#[serde(rename = "initialSearchTerm")]
|
||||||
|
pub intial_search: String,
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
device_pixel_ratio,
|
device_pixel_ratio,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
let selected_nodes_bounding_box_viewport = self.network_interface.selected_nodes_bounding_box_viewport(&self.breadcrumb_network_path);
|
|
||||||
let selected_visible_layers_bounding_box_viewport = self.selected_visible_layers_bounding_box_viewport();
|
|
||||||
match message {
|
match message {
|
||||||
// Sub-messages
|
// Sub-messages
|
||||||
DocumentMessage::Navigation(message) => {
|
DocumentMessage::Navigation(message) => {
|
||||||
@@ -191,11 +189,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
network_interface: &mut self.network_interface,
|
network_interface: &mut self.network_interface,
|
||||||
breadcrumb_network_path: &self.breadcrumb_network_path,
|
breadcrumb_network_path: &self.breadcrumb_network_path,
|
||||||
ipp,
|
ipp,
|
||||||
selection_bounds: if self.graph_view_overlay_open {
|
|
||||||
selected_nodes_bounding_box_viewport
|
|
||||||
} else {
|
|
||||||
selected_visible_layers_bounding_box_viewport
|
|
||||||
},
|
|
||||||
document_ptz: &mut self.document_ptz,
|
document_ptz: &mut self.document_ptz,
|
||||||
graph_view_overlay_open: self.graph_view_overlay_open,
|
graph_view_overlay_open: self.graph_view_overlay_open,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -259,7 +252,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
AlignAxis::X => DVec2::X,
|
AlignAxis::X => DVec2::X,
|
||||||
AlignAxis::Y => DVec2::Y,
|
AlignAxis::Y => DVec2::Y,
|
||||||
};
|
};
|
||||||
let Some(combined_box) = self.selected_visible_layers_bounding_box_viewport() else {
|
let Some(combined_box) = self.network_interface.selected_layers_artwork_bounding_box_viewport() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -486,7 +479,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
FlipAxis::X => DVec2::new(-1., 1.),
|
FlipAxis::X => DVec2::new(-1., 1.),
|
||||||
FlipAxis::Y => DVec2::new(1., -1.),
|
FlipAxis::Y => DVec2::new(1., -1.),
|
||||||
};
|
};
|
||||||
if let Some([min, max]) = self.selected_visible_and_unlock_layers_bounding_box_viewport() {
|
if let Some([min, max]) = self.network_interface.selected_unlocked_layers_bounding_box_viewport() {
|
||||||
let center = (max + min) / 2.;
|
let center = (max + min) / 2.;
|
||||||
let bbox_trans = DAffine2::from_translation(-center);
|
let bbox_trans = DAffine2::from_translation(-center);
|
||||||
let mut added_transaction = false;
|
let mut added_transaction = false;
|
||||||
@@ -506,7 +499,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
}
|
}
|
||||||
DocumentMessage::RotateSelectedLayers { degrees } => {
|
DocumentMessage::RotateSelectedLayers { degrees } => {
|
||||||
// Get the bounding box of selected layers in viewport space
|
// Get the bounding box of selected layers in viewport space
|
||||||
if let Some([min, max]) = self.selected_visible_and_unlock_layers_bounding_box_viewport() {
|
if let Some([min, max]) = self.network_interface.selected_unlocked_layers_bounding_box_viewport() {
|
||||||
// Calculate the center of the bounding box to use as rotation pivot
|
// Calculate the center of the bounding box to use as rotation pivot
|
||||||
let center = (max + min) / 2.;
|
let center = (max + min) / 2.;
|
||||||
// Transform that moves pivot point to origin
|
// Transform that moves pivot point to origin
|
||||||
@@ -1063,13 +1056,13 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
self.selected_layers_reorder(relative_index_offset, responses);
|
self.selected_layers_reorder(relative_index_offset, responses);
|
||||||
}
|
}
|
||||||
DocumentMessage::ClipLayer { id } => {
|
DocumentMessage::ClipLayer { id } => {
|
||||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
|
let layer = LayerNodeIdentifier::new(id, &self.network_interface);
|
||||||
|
|
||||||
responses.add(DocumentMessage::AddTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
responses.add(GraphOperationMessage::ClipModeToggle { layer });
|
responses.add(GraphOperationMessage::ClipModeToggle { layer });
|
||||||
}
|
}
|
||||||
DocumentMessage::SelectLayer { id, ctrl, shift } => {
|
DocumentMessage::SelectLayer { id, ctrl, shift } => {
|
||||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
|
let layer = LayerNodeIdentifier::new(id, &self.network_interface);
|
||||||
|
|
||||||
let mut nodes = vec![];
|
let mut nodes = vec![];
|
||||||
|
|
||||||
@@ -1266,7 +1259,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
}
|
}
|
||||||
DocumentMessage::ToggleLayerExpansion { id, recursive } => {
|
DocumentMessage::ToggleLayerExpansion { id, recursive } => {
|
||||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
|
let layer = LayerNodeIdentifier::new(id, &self.network_interface);
|
||||||
let metadata = self.metadata();
|
let metadata = self.metadata();
|
||||||
|
|
||||||
let is_collapsed = self.collapsed.0.contains(&layer);
|
let is_collapsed = self.collapsed.0.contains(&layer);
|
||||||
@@ -1323,7 +1316,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
self.network_interface.document_network().nodes.contains_key(node_id))
|
self.network_interface.document_network().nodes.contains_key(node_id))
|
||||||
.filter_map(|(node_id, click_targets)| {
|
.filter_map(|(node_id, click_targets)| {
|
||||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface, &[]);
|
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||||
(layer, click_targets)
|
(layer, click_targets)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1708,31 +1701,6 @@ impl DocumentMessageHandler {
|
|||||||
.last()
|
.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the combined bounding box of the click targets of the selected visible layers in viewport space
|
|
||||||
pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
|
||||||
self.network_interface
|
|
||||||
.selected_nodes()
|
|
||||||
.selected_visible_layers(&self.network_interface)
|
|
||||||
.filter_map(|layer| self.metadata().bounding_box_viewport(layer))
|
|
||||||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_visible_and_unlock_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
|
||||||
self.network_interface
|
|
||||||
.selected_nodes()
|
|
||||||
.selected_visible_and_unlocked_layers(&self.network_interface)
|
|
||||||
.filter_map(|layer| self.metadata().bounding_box_viewport(layer))
|
|
||||||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_visible_and_unlock_layers_bounding_box_document(&self) -> Option<[DVec2; 2]> {
|
|
||||||
self.network_interface
|
|
||||||
.selected_nodes()
|
|
||||||
.selected_visible_and_unlocked_layers(&self.network_interface)
|
|
||||||
.map(|layer| self.metadata().nonzero_bounding_box(layer))
|
|
||||||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn document_network(&self) -> &NodeNetwork {
|
pub fn document_network(&self) -> &NodeNetwork {
|
||||||
self.network_interface.document_network()
|
self.network_interface.document_network()
|
||||||
}
|
}
|
||||||
@@ -2741,7 +2709,22 @@ impl DocumentMessageHandler {
|
|||||||
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||||
.disabled(!has_selection || has_multiple_selection)
|
.disabled(!has_selection || has_multiple_selection)
|
||||||
.popover_layout({
|
.popover_layout({
|
||||||
let node_chooser = NodeCatalog::new()
|
// Showing only compatible types
|
||||||
|
let compatible_type = selected_layer.and_then(|layer| {
|
||||||
|
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);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut node_chooser = NodeCatalog::new();
|
||||||
|
node_chooser.intial_search = compatible_type.unwrap_or("".to_string());
|
||||||
|
|
||||||
|
let node_chooser = node_chooser
|
||||||
.on_update(move |node_type| {
|
.on_update(move |node_type| {
|
||||||
if let Some(layer) = selected_layer {
|
if let Some(layer) = selected_layer {
|
||||||
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
let primary_input = artboard.inputs.first().expect("Artboard should have a primary input").clone();
|
let primary_input = artboard.inputs.first().expect("Artboard should have a primary input").clone();
|
||||||
if let NodeInput::Node { node_id, .. } = &primary_input {
|
if let NodeInput::Node { node_id, .. } = &primary_input {
|
||||||
if network_interface.is_layer(node_id, &[]) && !network_interface.is_artboard(node_id, &[]) {
|
if network_interface.is_layer(node_id, &[]) && !network_interface.is_artboard(node_id, &[]) {
|
||||||
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(*node_id, network_interface, &[]), artboard_layer, 0, &[]);
|
network_interface.move_layer_to_stack(LayerNodeIdentifier::new(*node_id, network_interface), artboard_layer, 0, &[]);
|
||||||
} else {
|
} else {
|
||||||
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
|
network_interface.disconnect_input(&InputConnector::node(artboard_layer.to_node(), 0), &[]);
|
||||||
network_interface.set_input(&InputConnector::node(id, 0), primary_input, &[]);
|
network_interface.set_input(&InputConnector::node(id, 0), primary_input, &[]);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
|
pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
|
||||||
let new_merge_node = resolve_document_node_type("Merge").expect("Merge node").default_node_template();
|
let new_merge_node = resolve_document_node_type("Merge").expect("Merge node").default_node_template();
|
||||||
self.network_interface.insert_node(new_id, new_merge_node, &[]);
|
self.network_interface.insert_node(new_id, new_merge_node, &[]);
|
||||||
LayerNodeIdentifier::new(new_id, self.network_interface, &[])
|
LayerNodeIdentifier::new(new_id, self.network_interface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an artboard as the primary export for the document network
|
/// Creates an artboard as the primary export for the document network
|
||||||
@@ -138,7 +138,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
|
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
|
||||||
]);
|
]);
|
||||||
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
|
||||||
LayerNodeIdentifier::new(new_id, self.network_interface, &[])
|
LayerNodeIdentifier::new(new_id, self.network_interface)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
|
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
|
||||||
@@ -236,7 +236,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.layer_node.or_else(|| {
|
self.layer_node.or_else(|| {
|
||||||
let export_node = self.network_interface.document_network().exports.first().and_then(|export| export.as_node())?;
|
let export_node = self.network_interface.document_network().exports.first().and_then(|export| export.as_node())?;
|
||||||
if self.network_interface.is_layer(&export_node, &[]) {
|
if self.network_interface.is_layer(&export_node, &[]) {
|
||||||
Some(LayerNodeIdentifier::new(export_node, self.network_interface, &[]))
|
Some(LayerNodeIdentifier::new(export_node, self.network_interface))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ pub struct NavigationMessageContext<'a> {
|
|||||||
pub network_interface: &'a mut NodeNetworkInterface,
|
pub network_interface: &'a mut NodeNetworkInterface,
|
||||||
pub breadcrumb_network_path: &'a [NodeId],
|
pub breadcrumb_network_path: &'a [NodeId],
|
||||||
pub ipp: &'a InputPreprocessorMessageHandler,
|
pub ipp: &'a InputPreprocessorMessageHandler,
|
||||||
pub selection_bounds: Option<[DVec2; 2]>,
|
|
||||||
pub document_ptz: &'a mut PTZ,
|
pub document_ptz: &'a mut PTZ,
|
||||||
pub graph_view_overlay_open: bool,
|
pub graph_view_overlay_open: bool,
|
||||||
pub preferences: &'a PreferencesMessageHandler,
|
pub preferences: &'a PreferencesMessageHandler,
|
||||||
@@ -39,7 +38,6 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
|||||||
network_interface,
|
network_interface,
|
||||||
breadcrumb_network_path,
|
breadcrumb_network_path,
|
||||||
ipp,
|
ipp,
|
||||||
selection_bounds,
|
|
||||||
document_ptz,
|
document_ptz,
|
||||||
graph_view_overlay_open,
|
graph_view_overlay_open,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -386,9 +384,16 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
|||||||
responses.add(DocumentMessage::PTZUpdate);
|
responses.add(DocumentMessage::PTZUpdate);
|
||||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||||
}
|
}
|
||||||
|
// Fully zooms in on the selected
|
||||||
NavigationMessage::FitViewportToSelection => {
|
NavigationMessage::FitViewportToSelection => {
|
||||||
|
let selection_bounds = if graph_view_overlay_open {
|
||||||
|
network_interface.selected_nodes_bounding_box_viewport(breadcrumb_network_path)
|
||||||
|
} else {
|
||||||
|
network_interface.selected_layers_artwork_bounding_box_viewport()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(bounds) = selection_bounds {
|
if let Some(bounds) = selection_bounds {
|
||||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
let Some(ptz) = get_ptz(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||||
log::error!("Could not get node graph PTZ in FitViewportToSelection");
|
log::error!("Could not get node graph PTZ in FitViewportToSelection");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub enum NodeGraphMessage {
|
|||||||
nodes: Vec<(NodeId, NodeTemplate)>,
|
nodes: Vec<(NodeId, NodeTemplate)>,
|
||||||
new_ids: HashMap<NodeId, NodeId>,
|
new_ids: HashMap<NodeId, NodeId>,
|
||||||
},
|
},
|
||||||
|
AddPathNode,
|
||||||
AddImport,
|
AddImport,
|
||||||
AddExport,
|
AddExport,
|
||||||
Init,
|
Init,
|
||||||
@@ -81,6 +82,9 @@ pub enum NodeGraphMessage {
|
|||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
parent: LayerNodeIdentifier,
|
parent: LayerNodeIdentifier,
|
||||||
},
|
},
|
||||||
|
SetChainPosition {
|
||||||
|
node_id: NodeId,
|
||||||
|
},
|
||||||
PasteNodes {
|
PasteNodes {
|
||||||
serialized_nodes: String,
|
serialized_nodes: String,
|
||||||
},
|
},
|
||||||
@@ -97,6 +101,7 @@ pub enum NodeGraphMessage {
|
|||||||
PointerOutsideViewport {
|
PointerOutsideViewport {
|
||||||
shift: Key,
|
shift: Key,
|
||||||
},
|
},
|
||||||
|
ShakeNode,
|
||||||
RemoveImport {
|
RemoveImport {
|
||||||
import_index: usize,
|
import_index: usize,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,20 +10,24 @@ use crate::messages::portfolio::document::node_graph::utility_types::{ContextMen
|
|||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{
|
use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||||
self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
|
self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
|
||||||
};
|
};
|
||||||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
|
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
|
||||||
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
|
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_clip_mode};
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
|
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||||
|
use bezier_rs::Subpath;
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graphene_std::math::math_ext::QuadExt;
|
use graphene_std::math::math_ext::QuadExt;
|
||||||
|
use graphene_std::vector::misc::subpath_to_kurbo_bezpath;
|
||||||
use graphene_std::*;
|
use graphene_std::*;
|
||||||
|
use kurbo::{Line, Point};
|
||||||
use renderer::Quad;
|
use renderer::Quad;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
@@ -55,6 +59,8 @@ pub struct NodeGraphMessageHandler {
|
|||||||
/// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates,
|
/// If dragging the selected nodes, this stores the starting position both in viewport and node graph coordinates,
|
||||||
/// plus a flag indicating if it has been dragged since the mousedown began.
|
/// plus a flag indicating if it has been dragged since the mousedown began.
|
||||||
pub drag_start: Option<(DragStart, bool)>,
|
pub drag_start: Option<(DragStart, bool)>,
|
||||||
|
// Store the selected chain nodes on drag start so they can be reconnected if shaken
|
||||||
|
pub drag_start_chain_nodes: Vec<NodeId>,
|
||||||
/// If dragging the background to create a box selection, this stores its starting point in node graph coordinates,
|
/// If dragging the background to create a box selection, this stores its starting point in node graph coordinates,
|
||||||
/// plus a flag indicating if it has been dragged since the mousedown began.
|
/// plus a flag indicating if it has been dragged since the mousedown began.
|
||||||
box_selection_start: Option<(DVec2, bool)>,
|
box_selection_start: Option<(DVec2, bool)>,
|
||||||
@@ -119,6 +125,38 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
|
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
|
||||||
}
|
}
|
||||||
|
NodeGraphMessage::AddPathNode => {
|
||||||
|
let selected_nodes = network_interface.selected_nodes();
|
||||||
|
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
|
||||||
|
let first_layer = selected_layers.next();
|
||||||
|
let second_layer = selected_layers.next();
|
||||||
|
let has_single_selection = first_layer.is_some() && second_layer.is_none();
|
||||||
|
|
||||||
|
let compatible_type = first_layer.and_then(|layer| {
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &network_interface);
|
||||||
|
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
|
||||||
|
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
|
||||||
|
|
||||||
|
if first_layer.is_some() && has_single_selection && is_compatible {
|
||||||
|
if let Some(layer) = first_layer {
|
||||||
|
let node_type = "Path".to_string();
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &network_interface);
|
||||||
|
let is_modifiable = matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)));
|
||||||
|
if !is_modifiable {
|
||||||
|
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||||
|
node_type: node_type.clone(),
|
||||||
|
layer: LayerNodeIdentifier::new_unchecked(layer.to_node()),
|
||||||
|
});
|
||||||
|
responses.add(BroadcastEvent::SelectionChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
NodeGraphMessage::AddImport => {
|
NodeGraphMessage::AddImport => {
|
||||||
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path);
|
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path);
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
@@ -568,6 +606,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
|
NodeGraphMessage::MoveNodeToChainStart { node_id, parent } => {
|
||||||
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
|
network_interface.move_node_to_chain_start(&node_id, parent, selection_network_path);
|
||||||
}
|
}
|
||||||
|
NodeGraphMessage::SetChainPosition { node_id } => {
|
||||||
|
network_interface.set_chain_position(&node_id, selection_network_path);
|
||||||
|
}
|
||||||
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
||||||
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
|
let data = match serde_json::from_str::<Vec<(NodeId, NodeTemplate)>>(&serialized_nodes) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
@@ -821,6 +862,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.drag_start = Some((drag_start, false));
|
self.drag_start = Some((drag_start, false));
|
||||||
|
let selected_chain_nodes = updated_selected
|
||||||
|
.iter()
|
||||||
|
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.drag_start_chain_nodes = selected_chain_nodes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|selected| {
|
||||||
|
network_interface
|
||||||
|
.upstream_flow_back_from_nodes(vec![*selected], selection_network_path, FlowType::PrimaryFlow)
|
||||||
|
.skip(1)
|
||||||
|
.filter(|node_id| network_interface.is_chain(node_id, selection_network_path))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
self.begin_dragging = true;
|
self.begin_dragging = true;
|
||||||
self.node_has_moved_in_drag = false;
|
self.node_has_moved_in_drag = false;
|
||||||
self.update_node_graph_hints(responses);
|
self.update_node_graph_hints(responses);
|
||||||
@@ -1188,10 +1243,39 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
|
||||||
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
|
||||||
wire.rectangle_intersections_exist(bounding_box[0], bounding_box[1]).then_some((input, is_stack))
|
|
||||||
|
let bbox_rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
|
||||||
|
|
||||||
|
let p1 = DVec2::new(bbox_rect.x0, bbox_rect.y0);
|
||||||
|
let p2 = DVec2::new(bbox_rect.x1, bbox_rect.y0);
|
||||||
|
let p3 = DVec2::new(bbox_rect.x1, bbox_rect.y1);
|
||||||
|
let p4 = DVec2::new(bbox_rect.x0, bbox_rect.y1);
|
||||||
|
let ps = [p1, p2, p3, p4];
|
||||||
|
|
||||||
|
let inside = wire.is_inside_subpath(&Subpath::from_anchors_linear(ps, true), None, None);
|
||||||
|
|
||||||
|
let wire = subpath_to_kurbo_bezpath(wire);
|
||||||
|
|
||||||
|
let intersect = wire.segments().any(|segment| {
|
||||||
|
let rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
|
||||||
|
|
||||||
|
let top_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0));
|
||||||
|
let bottom_line = Line::new(Point::new(rect.x0, rect.y1), Point::new(rect.x1, rect.y1));
|
||||||
|
let left_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x0, rect.y1));
|
||||||
|
let right_line = Line::new(Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1));
|
||||||
|
|
||||||
|
!segment.intersect_line(top_line).is_empty()
|
||||||
|
|| !segment.intersect_line(bottom_line).is_empty()
|
||||||
|
|| !segment.intersect_line(left_line).is_empty()
|
||||||
|
|| !segment.intersect_line(right_line).is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
(intersect || inside).then_some((input, is_stack))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Prioritize vertical thick lines and cancel if there are multiple potential wires
|
// Prioritize vertical thick lines and cancel if there are multiple potential wires
|
||||||
let mut node_wires = Vec::new();
|
let mut node_wires = Vec::new();
|
||||||
let mut stack_wires = Vec::new();
|
let mut stack_wires = Vec::new();
|
||||||
@@ -1270,6 +1354,135 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
self.auto_panning.stop(&messages, responses);
|
self.auto_panning.stop(&messages, responses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NodeGraphMessage::ShakeNode => {
|
||||||
|
let Some(drag_start) = &self.drag_start else {
|
||||||
|
log::error!("Drag start should be initialized when shaking a node");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let viewport_location = ipp.mouse.position;
|
||||||
|
let point = network_metadata
|
||||||
|
.persistent_metadata
|
||||||
|
.navigation_metadata
|
||||||
|
.node_graph_to_viewport
|
||||||
|
.inverse()
|
||||||
|
.transform_point2(viewport_location);
|
||||||
|
|
||||||
|
// Collect the distance to move the shaken nodes after the undo
|
||||||
|
let graph_delta = IVec2::new(((point.x - drag_start.0.start_x) / 24.).round() as i32, ((point.y - drag_start.0.start_y) / 24.).round() as i32);
|
||||||
|
|
||||||
|
// Undo to the state of the graph before shaking
|
||||||
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
|
|
||||||
|
// Add a history step to abort to the state before shaking if right clicked
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
|
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
|
||||||
|
log::error!("Could not get selected nodes in ShakeNode");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut all_selected_nodes = selected_nodes.0.iter().copied().collect::<HashSet<_>>();
|
||||||
|
for selected_layer in selected_nodes
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.filter(|selected_node| network_interface.is_layer(selected_node, selection_network_path))
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
for sole_dependent in network_interface.upstream_nodes_below_layer(&selected_layer, selection_network_path) {
|
||||||
|
all_selected_nodes.insert(sole_dependent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for selected_node in &all_selected_nodes {
|
||||||
|
// Handle inputs of selected node
|
||||||
|
for input_index in 0..network_interface.number_of_inputs(selected_node, selection_network_path) {
|
||||||
|
let input_connector = InputConnector::node(*selected_node, input_index);
|
||||||
|
// Only disconnect inputs to non selected nodes
|
||||||
|
if network_interface
|
||||||
|
.upstream_output_connector(&input_connector, selection_network_path)
|
||||||
|
.and_then(|connector| connector.node_id())
|
||||||
|
.is_some_and(|node_id| !all_selected_nodes.contains(&node_id))
|
||||||
|
{
|
||||||
|
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let number_of_outputs = network_interface.number_of_outputs(selected_node, selection_network_path);
|
||||||
|
let first_deselected_upstream_node = network_interface
|
||||||
|
.upstream_flow_back_from_nodes(vec![*selected_node], selection_network_path, FlowType::PrimaryFlow)
|
||||||
|
.find(|upstream_node| !all_selected_nodes.contains(upstream_node));
|
||||||
|
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||||
|
log::error!("Could not get output wires in shake input");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disconnect output wires to non selected nodes
|
||||||
|
for output_index in 0..number_of_outputs {
|
||||||
|
let output_connector = OutputConnector::node(*selected_node, output_index);
|
||||||
|
if let Some(downstream_connections) = outward_wires.get(&output_connector) {
|
||||||
|
for &input_connector in downstream_connections {
|
||||||
|
if input_connector.node_id().is_some_and(|downstream_node| !all_selected_nodes.contains(&downstream_node)) {
|
||||||
|
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reconnection
|
||||||
|
// Find first non selected upstream node by primary flow
|
||||||
|
if let Some(first_deselected_upstream_node) = first_deselected_upstream_node {
|
||||||
|
let Some(downstream_connections_to_first_output) = outward_wires.get(&OutputConnector::node(*selected_node, 0)).cloned() else {
|
||||||
|
log::error!("Could not get downstream_connections_to_first_output in shake node");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Reconnect only if all downstream outputs are not selected
|
||||||
|
if !downstream_connections_to_first_output
|
||||||
|
.iter()
|
||||||
|
.any(|connector| connector.node_id().is_some_and(|node_id| all_selected_nodes.contains(&node_id)))
|
||||||
|
{
|
||||||
|
// Find what output on the deselected upstream node to reconnect to
|
||||||
|
for output_index in 0..network_interface.number_of_outputs(&first_deselected_upstream_node, selection_network_path) {
|
||||||
|
let output_connector = &OutputConnector::node(first_deselected_upstream_node, output_index);
|
||||||
|
let Some(outward_wires) = network_interface.outward_wires(selection_network_path) else {
|
||||||
|
log::error!("Could not get output wires in shake input");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(inputs) = outward_wires.get(output_connector) {
|
||||||
|
// This can only run once
|
||||||
|
if inputs.iter().any(|input_connector| {
|
||||||
|
input_connector
|
||||||
|
.node_id()
|
||||||
|
.is_some_and(|upstream_node| all_selected_nodes.contains(&upstream_node) && input_connector.input_index() == 0)
|
||||||
|
}) {
|
||||||
|
// Output index is the output of the deselected upstream node to reconnect to
|
||||||
|
for downstream_connections_to_first_output in &downstream_connections_to_first_output {
|
||||||
|
responses.add(NodeGraphMessage::CreateWire {
|
||||||
|
output_connector: OutputConnector::node(first_deselected_upstream_node, output_index),
|
||||||
|
input_connector: *downstream_connections_to_first_output,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all chain nodes back to chain position
|
||||||
|
// TODO: Fix
|
||||||
|
// for chain_node_to_reset in std::mem::take(&mut self.drag_start_chain_nodes) {
|
||||||
|
// responses.add(NodeGraphMessage::SetChainPosition { node_id: chain_node_to_reset });
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: false });
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
}
|
||||||
NodeGraphMessage::RemoveImport { import_index: usize } => {
|
NodeGraphMessage::RemoveImport { import_index: usize } => {
|
||||||
network_interface.remove_import(usize, selection_network_path);
|
network_interface.remove_import(usize, selection_network_path);
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
@@ -1354,6 +1567,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y {
|
if node_bbox[1].x >= document_bbox[0].x && node_bbox[0].x <= document_bbox[1].x && node_bbox[1].y >= document_bbox[0].y && node_bbox[0].y <= document_bbox[1].y {
|
||||||
nodes.push(*node_id);
|
nodes.push(*node_id);
|
||||||
}
|
}
|
||||||
|
for error in &self.node_graph_errors {
|
||||||
|
if error.node_path.contains(node_id) {
|
||||||
|
nodes.push(*node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(FrontendMessage::UpdateVisibleNodes { nodes });
|
responses.add(FrontendMessage::UpdateVisibleNodes { nodes });
|
||||||
@@ -1785,6 +2003,12 @@ impl NodeGraphMessageHandler {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.drag_start.is_some() {
|
||||||
|
common.extend(actions!(NodeGraphMessageDiscriminant;
|
||||||
|
ShakeNode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
common
|
common
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1824,26 +2048,57 @@ impl NodeGraphMessageHandler {
|
|||||||
let selection_all_locked = network_interface.selected_nodes().selected_unlocked_layers(network_interface).count() == 0;
|
let selection_all_locked = network_interface.selected_nodes().selected_unlocked_layers(network_interface).count() == 0;
|
||||||
let selection_all_visible = selected_nodes.selected_nodes().all(|node_id| network_interface.is_visible(node_id, breadcrumb_network_path));
|
let selection_all_visible = selected_nodes.selected_nodes().all(|node_id| network_interface.is_visible(node_id, breadcrumb_network_path));
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
let mut widgets = vec![
|
let mut widgets = vec![
|
||||||
PopoverButton::new()
|
PopoverButton::new()
|
||||||
.icon(Some("Node".to_string()))
|
.icon(Some("Node".to_string()))
|
||||||
.tooltip("New Node (Right Click)")
|
.tooltip("New Node (Right Click)")
|
||||||
.popover_layout({
|
.popover_layout({
|
||||||
let node_chooser = NodeCatalog::new()
|
// Showing only compatible types
|
||||||
.on_update(move |node_type| {
|
let compatible_type = match (selection_includes_layers, has_multiple_selection, selected_layer) {
|
||||||
let node_id = NodeId::new();
|
(true, false, Some(layer)) => {
|
||||||
|
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, &[]);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
Message::Batched {
|
let single_layer_selected = selection_includes_layers && !has_multiple_selection;
|
||||||
messages: Box::new([
|
|
||||||
NodeGraphMessage::CreateNodeFromContextMenu {
|
let mut node_chooser = NodeCatalog::new();
|
||||||
node_id: Some(node_id),
|
node_chooser.intial_search = compatible_type.unwrap_or("".to_string());
|
||||||
node_type: node_type.clone(),
|
|
||||||
xy: None,
|
let node_chooser = node_chooser
|
||||||
add_transaction: true,
|
.on_update(move |node_type| {
|
||||||
}
|
if let (true, Some(layer)) = (single_layer_selected, selected_layer) {
|
||||||
.into(),
|
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||||
NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into(),
|
node_type: node_type.clone(),
|
||||||
]),
|
layer: LayerNodeIdentifier::new_unchecked(layer.to_node()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
let node_id = NodeId::new();
|
||||||
|
Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
NodeGraphMessage::CreateNodeFromContextMenu {
|
||||||
|
node_id: Some(node_id),
|
||||||
|
node_type: node_type.clone(),
|
||||||
|
xy: None,
|
||||||
|
add_transaction: true,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.widget_holder();
|
.widget_holder();
|
||||||
@@ -2115,7 +2370,22 @@ impl NodeGraphMessageHandler {
|
|||||||
.icon(Some("Node".to_string()))
|
.icon(Some("Node".to_string()))
|
||||||
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||||
.popover_layout({
|
.popover_layout({
|
||||||
let node_chooser = NodeCatalog::new()
|
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface);
|
||||||
|
let compatible_type = {
|
||||||
|
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, &[]);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut node_chooser = NodeCatalog::new();
|
||||||
|
node_chooser.intial_search = compatible_type.unwrap_or("".to_string());
|
||||||
|
|
||||||
|
let node_chooser = node_chooser
|
||||||
.on_update(move |node_type| {
|
.on_update(move |node_type| {
|
||||||
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||||
node_type: node_type.clone(),
|
node_type: node_type.clone(),
|
||||||
@@ -2366,19 +2636,19 @@ impl NodeGraphMessageHandler {
|
|||||||
let mut ancestors_of_selected = HashSet::new();
|
let mut ancestors_of_selected = HashSet::new();
|
||||||
let mut descendants_of_selected = HashSet::new();
|
let mut descendants_of_selected = HashSet::new();
|
||||||
for selected_layer in &selected_layers {
|
for selected_layer in &selected_layers {
|
||||||
for ancestor in LayerNodeIdentifier::new(*selected_layer, network_interface, &[]).ancestors(network_interface.document_metadata()) {
|
for ancestor in LayerNodeIdentifier::new(*selected_layer, network_interface).ancestors(network_interface.document_metadata()) {
|
||||||
if ancestor != LayerNodeIdentifier::ROOT_PARENT && ancestor.to_node() != *selected_layer {
|
if ancestor != LayerNodeIdentifier::ROOT_PARENT && ancestor.to_node() != *selected_layer {
|
||||||
ancestors_of_selected.insert(ancestor.to_node());
|
ancestors_of_selected.insert(ancestor.to_node());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for descendant in LayerNodeIdentifier::new(*selected_layer, network_interface, &[]).descendants(network_interface.document_metadata()) {
|
for descendant in LayerNodeIdentifier::new(*selected_layer, network_interface).descendants(network_interface.document_metadata()) {
|
||||||
descendants_of_selected.insert(descendant.to_node());
|
descendants_of_selected.insert(descendant.to_node());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (&node_id, node_metadata) in &network_interface.document_network_metadata().persistent_metadata.node_metadata {
|
for (&node_id, node_metadata) in &network_interface.document_network_metadata().persistent_metadata.node_metadata {
|
||||||
if node_metadata.persistent_metadata.is_layer() {
|
if node_metadata.persistent_metadata.is_layer() {
|
||||||
let layer = LayerNodeIdentifier::new(node_id, network_interface, &[]);
|
let layer = LayerNodeIdentifier::new(node_id, network_interface);
|
||||||
|
|
||||||
let children_allowed =
|
let children_allowed =
|
||||||
// The layer has other layers as children along the secondary input's horizontal flow
|
// The layer has other layers as children along the secondary input's horizontal flow
|
||||||
@@ -2559,6 +2829,7 @@ impl Default for NodeGraphMessageHandler {
|
|||||||
node_has_moved_in_drag: false,
|
node_has_moved_in_drag: false,
|
||||||
shift_without_push: false,
|
shift_without_push: false,
|
||||||
box_selection_start: None,
|
box_selection_start: None,
|
||||||
|
drag_start_chain_nodes: Vec::new(),
|
||||||
selection_before_pointer_down: Vec::new(),
|
selection_before_pointer_down: Vec::new(),
|
||||||
disconnecting: None,
|
disconnecting: None,
|
||||||
initial_disconnecting: false,
|
initial_disconnecting: false,
|
||||||
|
|||||||
@@ -250,12 +250,8 @@ impl LayerNodeIdentifier {
|
|||||||
|
|
||||||
/// Construct a [`LayerNodeIdentifier`], debug asserting that it is a layer node. This should only be used in the document network since the structure is not loaded in nested networks.
|
/// Construct a [`LayerNodeIdentifier`], debug asserting that it is a layer node. This should only be used in the document network since the structure is not loaded in nested networks.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn new(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> Self {
|
pub fn new(node_id: NodeId, network_interface: &NodeNetworkInterface) -> Self {
|
||||||
debug_assert!(
|
debug_assert!(network_interface.is_layer(&node_id, &[]), "Layer identifier constructed from non-layer node {node_id}",);
|
||||||
network_interface.is_layer(&node_id, network_path),
|
|
||||||
"Layer identifier constructed from non-layer node {node_id}: {:#?}",
|
|
||||||
network_interface.nested_network(network_path).unwrap().nodes.get(&node_id)
|
|
||||||
);
|
|
||||||
Self::new_unchecked(node_id)
|
Self::new_unchecked(node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,12 +203,12 @@ impl NodeNetworkInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the first downstream layer(inclusive) from a node. If the node is a layer, it will return itself.
|
/// Returns the first downstream layer(inclusive) from a node. If the node is a layer, it will return itself.
|
||||||
pub fn downstream_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option<LayerNodeIdentifier> {
|
pub fn downstream_layer_for_chain_node(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option<NodeId> {
|
||||||
let mut id = *node_id;
|
let mut id = *node_id;
|
||||||
while !self.is_layer(&id, network_path) {
|
while !self.is_layer(&id, network_path) {
|
||||||
id = self.outward_wires(network_path)?.get(&OutputConnector::node(id, 0))?.first()?.node_id()?;
|
id = self.outward_wires(network_path)?.get(&OutputConnector::node(id, 0))?.first()?.node_id()?;
|
||||||
}
|
}
|
||||||
Some(LayerNodeIdentifier::new(id, self, network_path))
|
Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all downstream layers (inclusive) from a node. If the node is a layer, it will return itself.
|
/// Returns all downstream layers (inclusive) from a node. If the node is a layer, it will return itself.
|
||||||
@@ -388,8 +388,8 @@ impl NodeNetworkInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If a chain node does not have a selected downstream layer, then set the position to absolute
|
// If a chain node does not have a selected downstream layer, then set the position to absolute
|
||||||
let downstream_layer = self.downstream_layer(node_id, network_path);
|
let downstream_layer = self.downstream_layer_for_chain_node(node_id, network_path);
|
||||||
if downstream_layer.is_none_or(|downstream_layer| new_ids.keys().all(|key| *key != downstream_layer.to_node())) {
|
if downstream_layer.is_none_or(|downstream_layer| new_ids.keys().all(|key| *key != downstream_layer)) {
|
||||||
let Some(position) = self.position(node_id, network_path) else {
|
let Some(position) = self.position(node_id, network_path) else {
|
||||||
log::error!("Could not get position in create_node_template");
|
log::error!("Could not get position in create_node_template");
|
||||||
return None;
|
return None;
|
||||||
@@ -1244,7 +1244,7 @@ impl NodeNetworkInterface {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|reference| reference == "Artboard" && self.connected_to_output(node_id, &[]) && self.is_layer(node_id, &[]))
|
.is_some_and(|reference| reference == "Artboard" && self.connected_to_output(node_id, &[]) && self.is_layer(node_id, &[]))
|
||||||
{
|
{
|
||||||
Some(LayerNodeIdentifier::new(*node_id, self, &[]))
|
Some(LayerNodeIdentifier::new(*node_id, self))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -3025,7 +3025,7 @@ impl NodeNetworkInterface {
|
|||||||
|
|
||||||
// Helper functions for mutable getters
|
// Helper functions for mutable getters
|
||||||
impl NodeNetworkInterface {
|
impl NodeNetworkInterface {
|
||||||
pub fn upstream_chain_nodes(&mut self, network_path: &[NodeId]) -> Vec<NodeId> {
|
pub fn upstream_chain_nodes(&self, network_path: &[NodeId]) -> Vec<NodeId> {
|
||||||
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
||||||
log::error!("Could not get selected nodes in upstream_chain_nodes");
|
log::error!("Could not get selected nodes in upstream_chain_nodes");
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -3156,7 +3156,7 @@ impl NodeNetworkInterface {
|
|||||||
self.document_metadata.document_to_viewport = transform;
|
self.document_metadata.document_to_viewport = transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_eligible_to_be_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
pub fn is_eligible_to_be_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
|
||||||
let Some(node) = self.document_node(node_id, network_path) else {
|
let Some(node) = self.document_node(node_id, network_path) else {
|
||||||
log::error!("Could not get node {node_id} in is_eligible_to_be_layer");
|
log::error!("Could not get node {node_id} in is_eligible_to_be_layer");
|
||||||
return false;
|
return false;
|
||||||
@@ -3362,6 +3362,24 @@ impl NodeNetworkInterface {
|
|||||||
.map(|[a, b]| [node_graph_to_viewport.transform_point2(a), node_graph_to_viewport.transform_point2(b)])
|
.map(|[a, b]| [node_graph_to_viewport.transform_point2(a), node_graph_to_viewport.transform_point2(b)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected_layers_artwork_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||||
|
self.selected_nodes()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.filter(|node| self.is_layer(&node, &[]))
|
||||||
|
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||||
|
.reduce(Quad::combine_bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_unlocked_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||||
|
self.selected_nodes()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.filter(|node| self.is_layer(&node, &[]) && !self.is_layer(&node, &[]))
|
||||||
|
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||||
|
.reduce(Quad::combine_bounds)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the combined bounding box of the click targets of the selected nodes in the node graph in layer space
|
/// Get the combined bounding box of the click targets of the selected nodes in the node graph in layer space
|
||||||
pub fn selected_nodes_bounding_box(&mut self, network_path: &[NodeId]) -> Option<[DVec2; 2]> {
|
pub fn selected_nodes_bounding_box(&mut self, network_path: &[NodeId]) -> Option<[DVec2; 2]> {
|
||||||
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
let Some(selected_nodes) = self.selected_nodes_in_nested_network(network_path) else {
|
||||||
@@ -3451,7 +3469,7 @@ impl NodeNetworkInterface {
|
|||||||
|
|
||||||
let Some(first_root_layer) = self
|
let Some(first_root_layer) = self
|
||||||
.upstream_flow_back_from_nodes(vec![root_node.node_id], &[], FlowType::PrimaryFlow)
|
.upstream_flow_back_from_nodes(vec![root_node.node_id], &[], FlowType::PrimaryFlow)
|
||||||
.find_map(|node_id| if self.is_layer(&node_id, &[]) { Some(LayerNodeIdentifier::new(node_id, self, &[])) } else { None })
|
.find_map(|node_id| if self.is_layer(&node_id, &[]) { Some(LayerNodeIdentifier::new(node_id, self)) } else { None })
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -3467,7 +3485,7 @@ impl NodeNetworkInterface {
|
|||||||
if horizontal_root_node_id == first_root_layer.to_node() {
|
if horizontal_root_node_id == first_root_layer.to_node() {
|
||||||
for current_node_id in horizontal_flow_iter {
|
for current_node_id in horizontal_flow_iter {
|
||||||
if self.is_layer(¤t_node_id, &[]) {
|
if self.is_layer(¤t_node_id, &[]) {
|
||||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self, &[]);
|
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||||
if current_node_id == first_root_layer.to_node() {
|
if current_node_id == first_root_layer.to_node() {
|
||||||
awaiting_primary_flow.push((current_node_id, LayerNodeIdentifier::ROOT_PARENT));
|
awaiting_primary_flow.push((current_node_id, LayerNodeIdentifier::ROOT_PARENT));
|
||||||
@@ -3484,7 +3502,7 @@ impl NodeNetworkInterface {
|
|||||||
// Skip the horizontal_root_node_id node
|
// Skip the horizontal_root_node_id node
|
||||||
for current_node_id in horizontal_flow_iter.skip(1) {
|
for current_node_id in horizontal_flow_iter.skip(1) {
|
||||||
if self.is_layer(¤t_node_id, &[]) {
|
if self.is_layer(¤t_node_id, &[]) {
|
||||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self, &[]);
|
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||||
awaiting_primary_flow.push((current_node_id, parent_layer_node));
|
awaiting_primary_flow.push((current_node_id, parent_layer_node));
|
||||||
children.push((parent_layer_node, current_layer_node));
|
children.push((parent_layer_node, current_layer_node));
|
||||||
@@ -3505,7 +3523,7 @@ impl NodeNetworkInterface {
|
|||||||
for current_node_id in primary_flow_iter.skip(1) {
|
for current_node_id in primary_flow_iter.skip(1) {
|
||||||
if self.is_layer(¤t_node_id, &[]) {
|
if self.is_layer(¤t_node_id, &[]) {
|
||||||
// Create a new layer for the top of each stack, and add it as a child to the previous parent
|
// Create a new layer for the top of each stack, and add it as a child to the previous parent
|
||||||
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self, &[]);
|
let current_layer_node = LayerNodeIdentifier::new(current_node_id, self);
|
||||||
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
if !self.document_metadata.structure.contains_key(¤t_layer_node) {
|
||||||
children.push(current_layer_node);
|
children.push(current_layer_node);
|
||||||
|
|
||||||
@@ -3568,7 +3586,7 @@ impl NodeNetworkInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stack.extend(self_network_metadata.persistent_metadata.node_metadata.keys().map(|node_id| {
|
stack.extend(self_network_metadata.persistent_metadata.node_metadata.keys().map(|node_id| {
|
||||||
let mut current_path = path.clone();
|
let mut current_path: Vec<NodeId> = path.clone();
|
||||||
current_path.push(*node_id);
|
current_path.push(*node_id);
|
||||||
current_path
|
current_path
|
||||||
}));
|
}));
|
||||||
@@ -5085,12 +5103,45 @@ impl NodeNetworkInterface {
|
|||||||
else {
|
else {
|
||||||
log::error!("Could not set chain position for layer node {node_id}");
|
log::error!("Could not set chain position for layer node {node_id}");
|
||||||
}
|
}
|
||||||
|
// let previous_upstream_node = self.upstream_output_connector(&InputConnector::node(*node_id, 0), network_path).and_then(|output| output.node_id());
|
||||||
|
// let Some(previous_upstream_node_position) = previous_upstream_node.and_then(|upstream| self.position_from_downstream_node(&upstream, network_path)) else {
|
||||||
|
// log::error!("Could not get previous_upstream_node_position");
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
self.unload_upstream_node_click_targets(vec![*node_id], network_path);
|
self.unload_upstream_node_click_targets(vec![*node_id], network_path);
|
||||||
// Reload click target of the layer which encapsulate the chain
|
// Reload click target of the layer which encapsulate the chain
|
||||||
if let Some(downstream_layer) = self.downstream_layer(node_id, network_path) {
|
if let Some(downstream_layer) = self.downstream_layer_for_chain_node(node_id, network_path) {
|
||||||
self.unload_node_click_targets(&downstream_layer.to_node(), network_path);
|
self.unload_node_click_targets(&downstream_layer, network_path);
|
||||||
}
|
}
|
||||||
self.unload_all_nodes_bounding_box(network_path);
|
self.unload_all_nodes_bounding_box(network_path);
|
||||||
|
|
||||||
|
// let Some(new_upstream_node_position) = previous_upstream_node.and_then(|upstream| self.position_from_downstream_node(&upstream, network_path)) else {
|
||||||
|
// log::error!("Could not get new_upstream_node_position");
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
// if let Some(previous_upstream_node) = {
|
||||||
|
// let x_delta = new_upstream_node_position.x - previous_upstream_node_position.x;
|
||||||
|
// // Upstream node got shifted to left, so shift all upstream absolute sole dependents
|
||||||
|
// if x_delta != 0 {
|
||||||
|
// let upstream_absolute_nodes = SelectedNodes(
|
||||||
|
// self.upstream_flow_back_from_nodes(vec![previous_upstream_node], network_path, FlowType::UpstreamFlow)
|
||||||
|
// .into_iter()
|
||||||
|
// .filter(|node_id| self.is_absolute(node_id, network_path))
|
||||||
|
// .collect::<Vec<_>>(),
|
||||||
|
// );
|
||||||
|
// let old_selected_nodes = std::mem::replace(self.selected_nodes_mut(network_path).unwrap(), upstream_absolute_nodes);
|
||||||
|
// if x_delta < 0 {
|
||||||
|
// for _ in 0..x_delta.abs() {
|
||||||
|
// self.shift_selected_nodes(Direction::Left, false, network_path);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// for _ in 0..x_delta.abs() {
|
||||||
|
// self.shift_selected_nodes(Direction::Right, false, network_path);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// let _ = std::mem::replace(self.selected_nodes_mut(network_path).unwrap(), old_selected_nodes);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_upstream_chain_nodes(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<NodeId> {
|
fn valid_upstream_chain_nodes(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec<NodeId> {
|
||||||
@@ -5205,7 +5256,7 @@ impl NodeNetworkInterface {
|
|||||||
|
|
||||||
/// node_id is the first chain node, not the layer
|
/// node_id is the first chain node, not the layer
|
||||||
fn set_upstream_chain_to_absolute(&mut self, node_id: &NodeId, network_path: &[NodeId]) {
|
fn set_upstream_chain_to_absolute(&mut self, node_id: &NodeId, network_path: &[NodeId]) {
|
||||||
let Some(downstream_layer) = self.downstream_layer(node_id, network_path) else {
|
let Some(downstream_layer) = self.downstream_layer_for_chain_node(node_id, network_path) else {
|
||||||
log::error!("Could not get downstream layer in set_upstream_chain_to_absolute");
|
log::error!("Could not get downstream layer in set_upstream_chain_to_absolute");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -5218,7 +5269,7 @@ impl NodeNetworkInterface {
|
|||||||
if self.is_chain(upstream_id, network_path) {
|
if self.is_chain(upstream_id, network_path) {
|
||||||
self.set_absolute_position(upstream_id, previous_position, network_path);
|
self.set_absolute_position(upstream_id, previous_position, network_path);
|
||||||
// Reload click target of the layer which used to encapsulate the chain
|
// Reload click target of the layer which used to encapsulate the chain
|
||||||
self.unload_node_click_targets(&downstream_layer.to_node(), network_path);
|
self.unload_node_click_targets(&downstream_layer, network_path);
|
||||||
}
|
}
|
||||||
// If there is an upstream layer then stop breaking the chain
|
// If there is an upstream layer then stop breaking the chain
|
||||||
else {
|
else {
|
||||||
@@ -5297,8 +5348,8 @@ impl NodeNetworkInterface {
|
|||||||
// Deselect chain nodes upstream from a selected layer
|
// Deselect chain nodes upstream from a selected layer
|
||||||
if self.is_chain(selected_node, network_path)
|
if self.is_chain(selected_node, network_path)
|
||||||
&& self
|
&& self
|
||||||
.downstream_layer(selected_node, network_path)
|
.downstream_layer_for_chain_node(selected_node, network_path)
|
||||||
.is_some_and(|downstream_layer| node_ids.contains(&downstream_layer.to_node()))
|
.is_some_and(|downstream_layer| node_ids.contains(&downstream_layer))
|
||||||
{
|
{
|
||||||
node_ids.remove(selected_node);
|
node_ids.remove(selected_node);
|
||||||
}
|
}
|
||||||
@@ -5947,31 +5998,6 @@ impl NodeNetworkInterface {
|
|||||||
self.create_wire(&OutputConnector::node(*node_id, 0), &InputConnector::node(parent.to_node(), 1), network_path);
|
self.create_wire(&OutputConnector::node(*node_id, 0), &InputConnector::node(parent.to_node(), 1), network_path);
|
||||||
self.set_chain_position(node_id, network_path);
|
self.set_chain_position(node_id, network_path);
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement a more robust horizontal shift system when inserting a node into a chain.
|
|
||||||
// This should be done by breaking the chain and shifting the sole dependents for each node upstream of the insertion.
|
|
||||||
// Before inserting the node, shift the layer right 7 units so that all sole dependents are also shifted
|
|
||||||
// let input_connector = InputConnector::node(parent.to_node(), 0);
|
|
||||||
// let old_upstream = self.upstream_output_connector(&input_connector, network_path);
|
|
||||||
// This also needs to disconnect from the downstream layer
|
|
||||||
// self.disconnect_input(&input_connector, network_path);
|
|
||||||
// let Some(selected_nodes) = self.selected_nodes_mut(network_path) else {
|
|
||||||
// log::error!("Could not get selected nodes in move_layer_to_stack");
|
|
||||||
// return;
|
|
||||||
// };
|
|
||||||
// let old_selected_nodes = selected_nodes.replace_with(vec![parent.to_node()]);
|
|
||||||
|
|
||||||
// for _ in 0..7 {
|
|
||||||
// self.shift_selected_nodes(Direction::Left, false, network_path);
|
|
||||||
// }
|
|
||||||
// // Grip drag it back to the right
|
|
||||||
// for _ in 0..7 {
|
|
||||||
// self.shift_selected_nodes(Direction::Right, true, network_path);
|
|
||||||
// }
|
|
||||||
// let _ = self.selected_nodes_mut(network_path).unwrap().replace_with(old_selected_nodes);
|
|
||||||
// if let Some(old_upstream) = old_upstream {
|
|
||||||
// self.create_wire(&old_upstream, &input_connector, network_path);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Insert the node in the gap and set the upstream to a chain
|
// Insert the node in the gap and set the upstream to a chain
|
||||||
self.insert_node_between(node_id, &InputConnector::node(parent.to_node(), 1), 0, network_path);
|
self.insert_node_between(node_id, &InputConnector::node(parent.to_node(), 1), 0, network_path);
|
||||||
self.force_set_upstream_to_chain(node_id, network_path);
|
self.force_set_upstream_to_chain(node_id, network_path);
|
||||||
@@ -6778,13 +6804,6 @@ impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersisten
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
enum NodePersistentMetadataVersions {
|
|
||||||
DocumentNodePersistentMetadataPropertiesRow(DocumentNodePersistentMetadataPropertiesRow),
|
|
||||||
NodePersistentMetadataInputNames(DocumentNodePersistentMetadataInputNames),
|
|
||||||
NodePersistentMetadata(DocumentNodePersistentMetadata),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
|
fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub struct MenuBarMessageHandler {
|
|||||||
pub spreadsheet_view_open: bool,
|
pub spreadsheet_view_open: bool,
|
||||||
pub message_logging_verbosity: MessageLoggingVerbosity,
|
pub message_logging_verbosity: MessageLoggingVerbosity,
|
||||||
pub reset_node_definitions_on_open: bool,
|
pub reset_node_definitions_on_open: bool,
|
||||||
|
pub single_path_node_compatible_layer_selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
@@ -45,6 +46,7 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||||||
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
|
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
|
||||||
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
|
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
|
||||||
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
|
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
|
||||||
|
let single_path_node_compatible_layer_selected = self.single_path_node_compatible_layer_selected;
|
||||||
|
|
||||||
let menu_bar_entries = vec![
|
let menu_bar_entries = vec![
|
||||||
MenuBarEntry {
|
MenuBarEntry {
|
||||||
@@ -418,9 +420,8 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||||||
disabled: no_active_document || !has_selected_layers,
|
disabled: no_active_document || !has_selected_layers,
|
||||||
children: MenuBarEntryChildren(vec![{
|
children: MenuBarEntryChildren(vec![{
|
||||||
let list = <BooleanOperation as graphene_std::registry::ChoiceTypeStatic>::list();
|
let list = <BooleanOperation as graphene_std::registry::ChoiceTypeStatic>::list();
|
||||||
list.into_iter()
|
list.iter()
|
||||||
.map(|i| i.into_iter())
|
.flat_map(|i| i.iter())
|
||||||
.flatten()
|
|
||||||
.map(move |(operation, info)| MenuBarEntry {
|
.map(move |(operation, info)| MenuBarEntry {
|
||||||
label: info.label.to_string(),
|
label: info.label.to_string(),
|
||||||
icon: info.icon.as_ref().map(|i| i.to_string()),
|
icon: info.icon.as_ref().map(|i| i.to_string()),
|
||||||
@@ -436,6 +437,14 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||||||
..MenuBarEntry::default()
|
..MenuBarEntry::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
vec![MenuBarEntry {
|
||||||
|
label: "Make Path Editable".into(),
|
||||||
|
icon: Some("NodeShape".into()),
|
||||||
|
shortcut: None,
|
||||||
|
action: MenuBarEntry::create_action(|_| NodeGraphMessage::AddPathNode.into()),
|
||||||
|
disabled: !single_path_node_compatible_layer_selected,
|
||||||
|
..MenuBarEntry::default()
|
||||||
|
}],
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
MenuBarEntry::new_root(
|
MenuBarEntry::new_root(
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
|||||||
use crate::messages::portfolio::document_migration::*;
|
use crate::messages::portfolio::document_migration::*;
|
||||||
use crate::messages::preferences::SelectionMode;
|
use crate::messages::preferences::SelectionMode;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
@@ -78,6 +80,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
self.menu_bar_message_handler.has_selected_nodes = false;
|
self.menu_bar_message_handler.has_selected_nodes = false;
|
||||||
self.menu_bar_message_handler.has_selected_layers = false;
|
self.menu_bar_message_handler.has_selected_layers = false;
|
||||||
self.menu_bar_message_handler.has_selection_history = (false, false);
|
self.menu_bar_message_handler.has_selection_history = (false, false);
|
||||||
|
self.menu_bar_message_handler.single_path_node_compatible_layer_selected = false;
|
||||||
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
|
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
|
||||||
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
|
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
|
||||||
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
|
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
|
||||||
@@ -95,6 +98,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
|
let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
|
||||||
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
|
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
|
||||||
};
|
};
|
||||||
|
self.menu_bar_message_handler.single_path_node_compatible_layer_selected = {
|
||||||
|
let selected_nodes = document.network_interface.selected_nodes();
|
||||||
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
|
||||||
|
let first_layer = selected_layers.next();
|
||||||
|
let second_layer = selected_layers.next();
|
||||||
|
let has_single_selection = first_layer.is_some() && second_layer.is_none();
|
||||||
|
|
||||||
|
let compatible_type = first_layer.and_then(|layer| {
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||||
|
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
|
||||||
|
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
|
||||||
|
|
||||||
|
let is_modifiable = first_layer.map_or(false, |layer| {
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||||
|
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
|
||||||
|
});
|
||||||
|
|
||||||
|
first_layer.is_some() && has_single_selection && is_compatible && !is_modifiable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.menu_bar_message_handler.process_message(message, responses, ());
|
self.menu_bar_message_handler.process_message(message, responses, ());
|
||||||
@@ -762,6 +789,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
|
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
|
||||||
if node_graph_open {
|
if node_graph_open {
|
||||||
responses.add(NodeGraphMessage::UpdateGraphBarRight);
|
responses.add(NodeGraphMessage::UpdateGraphBarRight);
|
||||||
|
responses.add(NodeGraphMessage::UnloadWires);
|
||||||
|
responses.add(NodeGraphMessage::SendWires)
|
||||||
} else {
|
} else {
|
||||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1000,7 +1000,7 @@ impl ShapeState {
|
|||||||
} else {
|
} else {
|
||||||
// Push both in and out handles into the correct position
|
// Push both in and out handles into the correct position
|
||||||
for ((handle, sign), other_anchor) in handles.iter().zip([1., -1.]).zip(&anchor_positions) {
|
for ((handle, sign), other_anchor) in handles.iter().zip([1., -1.]).zip(&anchor_positions) {
|
||||||
let Some(anchor_vector) = other_anchor.map(|position| (position - anchor_position)) else {
|
let Some(anchor_vector) = other_anchor.map(|position| position - anchor_position) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
|
|||||||
use crate::messages::portfolio::document::utility_types::transformation::Axis;
|
use crate::messages::portfolio::document::utility_types::transformation::Axis;
|
||||||
use crate::messages::preferences::SelectionMode;
|
use crate::messages::preferences::SelectionMode;
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_widget, pivot_reference_point_widget};
|
||||||
use crate::messages::tool::common_functionality::shape_editor::{
|
use crate::messages::tool::common_functionality::shape_editor::{
|
||||||
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
|
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
|
||||||
@@ -18,6 +19,7 @@ use crate::messages::tool::common_functionality::shape_editor::{
|
|||||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
||||||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
|
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
|
||||||
use bezier_rs::{Bezier, BezierHandles, TValue};
|
use bezier_rs::{Bezier, BezierHandles, TValue};
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
@@ -264,6 +266,14 @@ impl LayoutHolder for PathTool {
|
|||||||
.selected_index(Some(self.options.path_overlay_mode as u32))
|
.selected_index(Some(self.options.path_overlay_mode as u32))
|
||||||
.widget_holder();
|
.widget_holder();
|
||||||
|
|
||||||
|
// Works only if a single layer is selected and its type is vectordata
|
||||||
|
let path_node_button = TextButton::new("Make Path Editable")
|
||||||
|
.icon(Some("NodeShape".into()))
|
||||||
|
.tooltip("Make Path Editable")
|
||||||
|
.on_update(|_| NodeGraphMessage::AddPathNode.into())
|
||||||
|
.disabled(!self.tool_data.single_path_node_compatible_layer_selected)
|
||||||
|
.widget_holder();
|
||||||
|
|
||||||
let [_checkbox, _dropdown] = {
|
let [_checkbox, _dropdown] = {
|
||||||
let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path);
|
let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path);
|
||||||
[pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()]
|
[pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()]
|
||||||
@@ -294,6 +304,7 @@ impl LayoutHolder for PathTool {
|
|||||||
unrelated_seperator.clone(),
|
unrelated_seperator.clone(),
|
||||||
path_overlay_mode_widget,
|
path_overlay_mode_widget,
|
||||||
unrelated_seperator.clone(),
|
unrelated_seperator.clone(),
|
||||||
|
path_node_button,
|
||||||
// checkbox.clone(),
|
// checkbox.clone(),
|
||||||
// related_seperator.clone(),
|
// related_seperator.clone(),
|
||||||
// dropdown.clone(),
|
// dropdown.clone(),
|
||||||
@@ -522,6 +533,7 @@ struct PathToolData {
|
|||||||
drill_through_cycle_count: usize,
|
drill_through_cycle_count: usize,
|
||||||
hovered_layers: Vec<LayerNodeIdentifier>,
|
hovered_layers: Vec<LayerNodeIdentifier>,
|
||||||
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
|
ghost_outline: Vec<(Vec<ClickTargetType>, DAffine2)>,
|
||||||
|
single_path_node_compatible_layer_selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathToolData {
|
impl PathToolData {
|
||||||
@@ -2383,6 +2395,31 @@ impl Fsm for PathToolFsmState {
|
|||||||
point_select_state: shape_editor.get_dragging_state(&document.network_interface),
|
point_select_state: shape_editor.get_dragging_state(&document.network_interface),
|
||||||
colinear,
|
colinear,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tool_data.single_path_node_compatible_layer_selected = {
|
||||||
|
let selected_nodes = document.network_interface.selected_nodes();
|
||||||
|
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
|
||||||
|
let first_layer = selected_layers.next();
|
||||||
|
let second_layer = selected_layers.next();
|
||||||
|
let has_single_selection = first_layer.is_some() && second_layer.is_none();
|
||||||
|
|
||||||
|
let compatible_type = first_layer.and_then(|layer| {
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||||
|
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
|
||||||
|
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
|
||||||
|
Some(format!("type:{}", output_type.nested_type()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
|
||||||
|
|
||||||
|
let is_modifiable = first_layer.map_or(false, |layer| {
|
||||||
|
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||||
|
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
|
||||||
|
});
|
||||||
|
|
||||||
|
first_layer.is_some() && has_single_selection && is_compatible && !is_modifiable
|
||||||
|
};
|
||||||
tool_data.update_selection_status(shape_editor, document);
|
tool_data.update_selection_status(shape_editor, document);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1323,7 +1323,7 @@ mod test_transform_layer {
|
|||||||
let document = editor.active_document_mut();
|
let document = editor.active_document_mut();
|
||||||
let group_children = document.network_interface.downstream_layers(&group_layer.to_node(), &[]);
|
let group_children = document.network_interface.downstream_layers(&group_layer.to_node(), &[]);
|
||||||
if !group_children.is_empty() {
|
if !group_children.is_empty() {
|
||||||
Some(LayerNodeIdentifier::new(group_children[0], &document.network_interface, &[]))
|
Some(LayerNodeIdentifier::new(group_children[0], &document.network_interface))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
"lint-fix": "eslint . --fix && tsc --noEmit",
|
"lint-fix": "eslint . --fix && tsc --noEmit",
|
||||||
"---------- INTERNAL ----------": "",
|
"---------- INTERNAL ----------": "",
|
||||||
"setup": "node package-installer.js",
|
"setup": "node package-installer.js",
|
||||||
"tauri:dev": "vite",
|
|
||||||
"tauri:build": "wasm-pack build ./wasm --target=web --features=tauri",
|
|
||||||
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
|
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
|
||||||
"wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web",
|
"wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web",
|
||||||
"wasm:build-production": "wasm-pack build ./wasm --release --target=web",
|
"wasm:build-production": "wasm-pack build ./wasm --release --target=web",
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "graphite-desktop"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Graphite Desktop"
|
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
|
||||||
license = "Apache-2.0"
|
|
||||||
repository = ""
|
|
||||||
default-run = "graphite-desktop"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.79"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# By default Tauri runs in production mode when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
|
||||||
default = ["custom-protocol", "gpu"]
|
|
||||||
# This feature is used for production builds where `devPath` points to the filesystem
|
|
||||||
# DO NOT remove this
|
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
|
||||||
gpu = ["graphite-editor/gpu"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Local dependencies
|
|
||||||
graphite-editor = { path = "../../editor", features = [
|
|
||||||
"gpu",
|
|
||||||
"ron",
|
|
||||||
"vello",
|
|
||||||
"decouple-execution",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# Workspace dependencies
|
|
||||||
axum = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
ron = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
fern = { workspace = true }
|
|
||||||
futures = { workspace = true }
|
|
||||||
|
|
||||||
# Required dependencies
|
|
||||||
tauri = { version = "2", features = ["devtools", "wry"] }
|
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
tauri-plugin-http = "2"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
# Required dependencies
|
|
||||||
tauri-build = { version = "2", features = [] }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Directory required for compilation, but not tracked by git if empty.
|
|
||||||
let dist_dir: PathBuf = ["..", "dist"].iter().collect();
|
|
||||||
fs::create_dir_all(dist_dir).unwrap();
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"identifier": "desktop-capability",
|
|
||||||
"platforms": ["macOS", "windows", "linux"],
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": ["http:default"]
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"identifier": "migrated",
|
|
||||||
"description": "permissions that were migrated from v1",
|
|
||||||
"local": true,
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": [
|
|
||||||
"core:default",
|
|
||||||
"core:window:allow-create",
|
|
||||||
"core:window:allow-center",
|
|
||||||
"core:window:allow-request-user-attention",
|
|
||||||
"core:window:allow-set-resizable",
|
|
||||||
"core:window:allow-set-maximizable",
|
|
||||||
"core:window:allow-set-minimizable",
|
|
||||||
"core:window:allow-set-closable",
|
|
||||||
"core:window:allow-set-title",
|
|
||||||
"core:window:allow-maximize",
|
|
||||||
"core:window:allow-unmaximize",
|
|
||||||
"core:window:allow-minimize",
|
|
||||||
"core:window:allow-unminimize",
|
|
||||||
"core:window:allow-show",
|
|
||||||
"core:window:allow-hide",
|
|
||||||
"core:window:allow-close",
|
|
||||||
"core:window:allow-set-decorations",
|
|
||||||
"core:window:allow-set-always-on-top",
|
|
||||||
"core:window:allow-set-content-protected",
|
|
||||||
"core:window:allow-set-size",
|
|
||||||
"core:window:allow-set-min-size",
|
|
||||||
"core:window:allow-set-max-size",
|
|
||||||
"core:window:allow-set-position",
|
|
||||||
"core:window:allow-set-fullscreen",
|
|
||||||
"core:window:allow-set-focus",
|
|
||||||
"core:window:allow-set-icon",
|
|
||||||
"core:window:allow-set-skip-taskbar",
|
|
||||||
"core:window:allow-set-cursor-grab",
|
|
||||||
"core:window:allow-set-cursor-visible",
|
|
||||||
"core:window:allow-set-cursor-icon",
|
|
||||||
"core:window:allow-set-cursor-position",
|
|
||||||
"core:window:allow-set-ignore-cursor-events",
|
|
||||||
"core:window:allow-start-dragging",
|
|
||||||
"core:webview:allow-print",
|
|
||||||
"shell:allow-execute",
|
|
||||||
"shell:allow-open",
|
|
||||||
"http:default",
|
|
||||||
"core:app:allow-app-show",
|
|
||||||
"core:app:allow-app-hide",
|
|
||||||
"shell:default",
|
|
||||||
"http:default"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,84 +0,0 @@
|
|||||||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
|
||||||
|
|
||||||
use axum::routing::get;
|
|
||||||
use axum::Router;
|
|
||||||
use fern::colors::{Color, ColoredLevelConfig};
|
|
||||||
use graphite_editor::node_graph_executor::*;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
static NODE_RUNTIME_IO: Mutex<Option<NodeRuntimeIO>> = const { Mutex::new(None) };
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
println!("Starting server...");
|
|
||||||
|
|
||||||
let colors = ColoredLevelConfig::new().debug(Color::Magenta).info(Color::Green).error(Color::Red);
|
|
||||||
|
|
||||||
fern::Dispatch::new()
|
|
||||||
.chain(std::io::stdout())
|
|
||||||
.level(log::LevelFilter::Trace)
|
|
||||||
.level_for("naga", log::LevelFilter::Error)
|
|
||||||
.level_for("wgpu-hal", log::LevelFilter::Error)
|
|
||||||
.level_for("wgpu_hal", log::LevelFilter::Error)
|
|
||||||
.level_for("wgpu_core", log::LevelFilter::Error)
|
|
||||||
.format(move |out, message, record| {
|
|
||||||
out.finish(format_args!(
|
|
||||||
"[{}]{} {} {}",
|
|
||||||
// This will color the log level only, not the whole line. Just a touch.
|
|
||||||
colors.color(record.level()),
|
|
||||||
chrono::Utc::now().format("[%Y-%m-%d %H:%M:%S]"),
|
|
||||||
message,
|
|
||||||
record.module_path().unwrap_or("")
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.apply()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
std::thread::spawn(|| loop {
|
|
||||||
futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph());
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(16))
|
|
||||||
});
|
|
||||||
graphite_editor::application::set_uuid_seed(0);
|
|
||||||
|
|
||||||
let mut runtime_lock = NODE_RUNTIME_IO.lock().unwrap();
|
|
||||||
*runtime_lock = Some(NodeRuntimeIO::new());
|
|
||||||
drop(runtime_lock);
|
|
||||||
|
|
||||||
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
|
|
||||||
|
|
||||||
// Run it with hyper on localhost:3000
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_http::init())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.invoke_handler(tauri::generate_handler![poll_node_graph, runtime_message])
|
|
||||||
.setup(|_app| {
|
|
||||||
use tauri::Manager;
|
|
||||||
_app.get_webview_window("main").unwrap().open_devtools();
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
|
||||||
#[tauri::command]
|
|
||||||
fn poll_node_graph() -> String {
|
|
||||||
let vec: Vec<_> = NODE_RUNTIME_IO.lock().as_mut().unwrap().as_mut().unwrap().receive().collect();
|
|
||||||
ron::to_string(&vec).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn runtime_message(message: String) -> Result<(), String> {
|
|
||||||
let message = match ron::from_str(&message) {
|
|
||||||
Ok(message) => message,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to deserialize message: {}\nwith error: {}", message, e);
|
|
||||||
return Err("Failed to deserialize message".into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let response = NODE_RUNTIME_IO.lock().as_ref().unwrap().as_ref().unwrap().send(message);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
|
||||||
"build": {
|
|
||||||
"beforeBuildCommand": "npm run tauri:build",
|
|
||||||
"beforeDevCommand": "npm run tauri:dev",
|
|
||||||
"frontendDist": "../dist",
|
|
||||||
"devUrl": "http://127.0.0.1:8080/"
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": true,
|
|
||||||
"category": "DeveloperTool",
|
|
||||||
"copyright": "",
|
|
||||||
"targets": "all",
|
|
||||||
"externalBin": [],
|
|
||||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
|
||||||
"windows": {
|
|
||||||
"certificateThumbprint": null,
|
|
||||||
"digestAlgorithm": "sha256",
|
|
||||||
"timestampUrl": ""
|
|
||||||
},
|
|
||||||
"longDescription": "",
|
|
||||||
"macOS": {
|
|
||||||
"entitlements": null,
|
|
||||||
"exceptionDomain": "",
|
|
||||||
"frameworks": [],
|
|
||||||
"providerShortName": null,
|
|
||||||
"signingIdentity": null
|
|
||||||
},
|
|
||||||
"resources": [],
|
|
||||||
"shortDescription": "",
|
|
||||||
"linux": {
|
|
||||||
"deb": {
|
|
||||||
"depends": ["librustc_codegen_spirv"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"productName": "Graphite",
|
|
||||||
"mainBinaryName": "Graphite",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"identifier": "rs.graphite.editor",
|
|
||||||
"plugins": {},
|
|
||||||
"app": {
|
|
||||||
"withGlobalTauri": true,
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"decorations": false,
|
|
||||||
"fullscreen": false,
|
|
||||||
"height": 1080,
|
|
||||||
"resizable": true,
|
|
||||||
"title": "Graphite",
|
|
||||||
"width": 1920,
|
|
||||||
"useHttpsScheme": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
|
||||||
"csp": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,6 +36,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||||||
let textToolInteractiveInputElement = undefined as undefined | HTMLDivElement;
|
let textToolInteractiveInputElement = undefined as undefined | HTMLDivElement;
|
||||||
let canvasFocused = true;
|
let canvasFocused = true;
|
||||||
let inPointerLock = false;
|
let inPointerLock = false;
|
||||||
|
const shakeSamples: { x: number; y: number; time: number }[] = [];
|
||||||
|
let lastShakeTime = 0;
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
|
|
||||||
@@ -159,6 +161,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||||||
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
|
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
|
||||||
|
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||||
|
if (detectShake(e)) editor.handle.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers);
|
||||||
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
|
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +334,71 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectShake(e: PointerEvent | MouseEvent): boolean {
|
||||||
|
const SENSITIVITY_DIRECTION_CHANGES = 3;
|
||||||
|
const SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO = 0.1;
|
||||||
|
const DETECTION_WINDOW_MS = 500;
|
||||||
|
const DEBOUNCE_MS = 1000;
|
||||||
|
|
||||||
|
// Add the current mouse position and time to our list of samples
|
||||||
|
const now = Date.now();
|
||||||
|
shakeSamples.push({ x: e.clientX, y: e.clientY, time: now });
|
||||||
|
|
||||||
|
// Remove samples that are older than our time window
|
||||||
|
while (shakeSamples.length > 0 && now - shakeSamples[0].time > DETECTION_WINDOW_MS) {
|
||||||
|
shakeSamples.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't be shaking if it's too early in terms of samples or debounce time
|
||||||
|
if (shakeSamples.length <= 3 || now - lastShakeTime <= DEBOUNCE_MS) return false;
|
||||||
|
|
||||||
|
// Calculate the total distance traveled
|
||||||
|
let totalDistanceSquared = 0;
|
||||||
|
for (let i = 1; i < shakeSamples.length; i += 1) {
|
||||||
|
const p1 = shakeSamples[i - 1];
|
||||||
|
const p2 = shakeSamples[i];
|
||||||
|
totalDistanceSquared += (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the number of times the mouse changes direction significantly, and the average position of the mouse
|
||||||
|
let directionChanges = 0;
|
||||||
|
const averagePoint = { x: 0, y: 0 };
|
||||||
|
let averagePointCount = 0;
|
||||||
|
for (let i = 0; i < shakeSamples.length - 2; i += 1) {
|
||||||
|
const p1 = shakeSamples[i];
|
||||||
|
const p2 = shakeSamples[i + 1];
|
||||||
|
const p3 = shakeSamples[i + 2];
|
||||||
|
|
||||||
|
const vector1 = { x: p2.x - p1.x, y: p2.y - p1.y };
|
||||||
|
const vector2 = { x: p3.x - p2.x, y: p3.y - p2.y };
|
||||||
|
|
||||||
|
// Check if the dot product is negative, which indicates the angle between vectors is > 90 degrees
|
||||||
|
if (vector1.x * vector2.x + vector1.y * vector2.y < 0) directionChanges += 1;
|
||||||
|
|
||||||
|
averagePoint.x += p2.x;
|
||||||
|
averagePoint.y += p2.y;
|
||||||
|
averagePointCount += 1;
|
||||||
|
}
|
||||||
|
if (averagePointCount > 0) {
|
||||||
|
averagePoint.x /= averagePointCount;
|
||||||
|
averagePoint.y /= averagePointCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the displacement (the distance between the first and last mouse positions)
|
||||||
|
const lastPoint = shakeSamples[shakeSamples.length - 1];
|
||||||
|
const displacementSquared = (lastPoint.x - averagePoint.x) ** 2 + (lastPoint.y - averagePoint.y) ** 2;
|
||||||
|
|
||||||
|
// A shake is detected if the mouse has traveled a lot but not moved far, and has changed direction enough times
|
||||||
|
if (SENSITIVITY_DISTANCE_TO_DISPLACEMENT_RATIO * totalDistanceSquared >= displacementSquared && directionChanges >= SENSITIVITY_DIRECTION_CHANGES) {
|
||||||
|
lastShakeTime = now;
|
||||||
|
shakeSamples.length = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Frontend message subscriptions
|
// Frontend message subscriptions
|
||||||
|
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerPaste, async () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "graphite-wasm"
|
name = "graphite-wasm"
|
||||||
publish = false
|
publish = false
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.88"
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
readme = "../../README.md"
|
readme = "../../README.md"
|
||||||
@@ -13,7 +13,7 @@ license = "Apache-2.0"
|
|||||||
[features]
|
[features]
|
||||||
default = ["gpu"]
|
default = ["gpu"]
|
||||||
gpu = ["editor/gpu"]
|
gpu = ["editor/gpu"]
|
||||||
tauri = [ "editor/tauri"]
|
tauri = ["editor/tauri"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|||||||
@@ -384,6 +384,17 @@ impl EditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mouse shaken
|
||||||
|
#[wasm_bindgen(js_name = onMouseShake)]
|
||||||
|
pub fn on_mouse_shake(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
|
||||||
|
let editor_mouse_state = EditorMouseState::from_keys_and_editor_position(mouse_keys, (x, y).into());
|
||||||
|
|
||||||
|
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
||||||
|
|
||||||
|
let message = InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Mouse double clicked
|
/// Mouse double clicked
|
||||||
#[wasm_bindgen(js_name = onDoubleClick)]
|
#[wasm_bindgen(js_name = onDoubleClick)]
|
||||||
pub fn on_double_click(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
|
pub fn on_double_click(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ where
|
|||||||
A::Item: Clone,
|
A::Item: Clone,
|
||||||
B::Item: Clone,
|
B::Item: Clone,
|
||||||
{
|
{
|
||||||
a.flat_map(move |i| (b.clone().map(move |j| (i.clone(), j))))
|
a.flat_map(move |i| b.clone().map(move |j| (i.clone(), j)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
||||||
|
|||||||
@@ -1086,7 +1086,7 @@ fn compute_dual(minor_graph: &MinorGraph) -> Result<DualGraph, BooleanError> {
|
|||||||
let outer_face_key = if count != 1 {
|
let outer_face_key = if count != 1 {
|
||||||
#[cfg(feature = "logging")]
|
#[cfg(feature = "logging")]
|
||||||
eprintln!("Found multiple outer faces: {areas:?}, falling back to area calculation");
|
eprintln!("Found multiple outer faces: {areas:?}, falling back to area calculation");
|
||||||
let (key, _) = *areas.iter().max_by_key(|(_, area)| ((area.abs() * 1000.) as u64)).unwrap();
|
let (key, _) = *areas.iter().max_by_key(|(_, area)| (area.abs() * 1000.) as u64).unwrap();
|
||||||
*key
|
*key
|
||||||
} else {
|
} else {
|
||||||
*windings
|
*windings
|
||||||
|
|||||||
@@ -835,6 +835,20 @@ impl Color {
|
|||||||
[(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8]
|
[(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the all RGB components as a u8 slice, first component is red, followed by green, followed by blue. Use this if the [`Color`] is in linear space.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use graphene_core::color::Color;
|
||||||
|
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||||
|
/// // TODO: Add test
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn to_rgb8_srgb(&self) -> [u8; 3] {
|
||||||
|
let gamma = self.to_gamma_srgb();
|
||||||
|
[(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8]
|
||||||
|
}
|
||||||
|
|
||||||
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
||||||
/// Convert a [Color] to a hue, saturation, lightness and alpha (all between 0 and 1)
|
/// Convert a [Color] to a hue, saturation, lightness and alpha (all between 0 and 1)
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -27,6 +27,24 @@ impl<T> Instances<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_instance(instance: Instance<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
instance: vec![instance.instance],
|
||||||
|
transform: vec![instance.transform],
|
||||||
|
alpha_blending: vec![instance.alpha_blending],
|
||||||
|
source_node_id: vec![instance.source_node_id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
instance: Vec::with_capacity(capacity),
|
||||||
|
transform: Vec::with_capacity(capacity),
|
||||||
|
alpha_blending: Vec::with_capacity(capacity),
|
||||||
|
source_node_id: Vec::with_capacity(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, instance: Instance<T>) {
|
pub fn push(&mut self, instance: Instance<T>) {
|
||||||
self.instance.push(instance.instance);
|
self.instance.push(instance.instance);
|
||||||
self.transform.push(instance.transform);
|
self.transform.push(instance.transform);
|
||||||
@@ -161,6 +179,18 @@ unsafe impl<T: StaticType + 'static> StaticType for Instances<T> {
|
|||||||
type Static = Instances<T>;
|
type Static = Instances<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> FromIterator<Instance<T>> for Instances<T> {
|
||||||
|
fn from_iter<I: IntoIterator<Item = Instance<T>>>(iter: I) -> Self {
|
||||||
|
let iter = iter.into_iter();
|
||||||
|
let (lower, _) = iter.size_hint();
|
||||||
|
let mut instances = Self::with_capacity(lower);
|
||||||
|
for instance in iter {
|
||||||
|
instances.push(instance);
|
||||||
|
}
|
||||||
|
instances
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn one_daffine2_default() -> Vec<DAffine2> {
|
fn one_daffine2_default() -> Vec<DAffine2> {
|
||||||
vec![DAffine2::IDENTITY]
|
vec![DAffine2::IDENTITY]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,18 @@ use crate::{Context, Ctx};
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
|
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, VectorDataTable)] value: T) -> String {
|
||||||
format!("{:?}", value)
|
format!("{:?}", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Text"))]
|
||||||
|
fn serialize<T: serde::Serialize>(
|
||||||
|
_: impl Ctx,
|
||||||
|
#[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, Color, Option<Color>, GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] value: T,
|
||||||
|
) -> String {
|
||||||
|
serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
|
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
|
||||||
first.clone() + &second
|
first.clone() + &second
|
||||||
|
|||||||
@@ -50,16 +50,14 @@ impl PathBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if per_glyph_instances {
|
if per_glyph_instances {
|
||||||
if !self.glyph_subpaths.is_empty() {
|
self.vector_table.push(Instance {
|
||||||
self.vector_table.push(Instance {
|
instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
||||||
instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
transform: DAffine2::from_translation(glyph_offset),
|
||||||
transform: DAffine2::from_translation(glyph_offset),
|
..Default::default()
|
||||||
..Default::default()
|
});
|
||||||
})
|
} else {
|
||||||
}
|
for subpath in self.glyph_subpaths.drain(..) {
|
||||||
} else if !self.glyph_subpaths.is_empty() {
|
// Unwrapping here is ok because `self.vector_table` is initialized with a single `VectorData`
|
||||||
for subpath in self.glyph_subpaths.iter() {
|
|
||||||
// Unwrapping here is ok, since the check above guarantees there is at least one `VectorData`
|
|
||||||
self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false);
|
self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,18 +126,26 @@ fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder
|
|||||||
|
|
||||||
// User-requested tilt applied around baseline to avoid vertical displacement
|
// User-requested tilt applied around baseline to avoid vertical displacement
|
||||||
// Translation ensures rotation point is at the baseline, not origin
|
// Translation ensures rotation point is at the baseline, not origin
|
||||||
let skew = DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
let skew = if per_glyph_instances {
|
||||||
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64));
|
} else {
|
||||||
|
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||||
|
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||||
|
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||||
|
};
|
||||||
|
|
||||||
let synthesis = run.synthesis();
|
let synthesis = run.synthesis();
|
||||||
|
|
||||||
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
||||||
// This preserves the distinction between font styling and user transformations
|
// This preserves the distinction between font styling and user transformations
|
||||||
let style_skew = synthesis.skew().map(|angle| {
|
let style_skew = synthesis.skew().map(|angle| {
|
||||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
if per_glyph_instances {
|
||||||
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
} else {
|
||||||
|
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||||
|
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||||
|
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let font = run.font();
|
let font = run.font();
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ where
|
|||||||
A::Item: Clone,
|
A::Item: Clone,
|
||||||
B::Item: Clone,
|
B::Item: Clone,
|
||||||
{
|
{
|
||||||
a.flat_map(move |i| (b.clone().map(move |j| (i.clone(), j))))
|
a.flat_map(move |i| b.clone().map(move |j| (i.clone(), j)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use bezier_rs::BezierHandles;
|
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use kurbo::{CubicBez, Line, PathSeg, Point, QuadBez};
|
use kurbo::{BezPath, CubicBez, Line, PathSeg, Point, QuadBez};
|
||||||
|
|
||||||
|
use super::PointId;
|
||||||
|
|
||||||
/// Represents different ways of calculating the centroid.
|
/// Represents different ways of calculating the centroid.
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||||
@@ -138,3 +140,39 @@ pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn subpath_to_kurbo_bezpath(subpath: Subpath<PointId>) -> BezPath {
|
||||||
|
let maniputor_groups = subpath.manipulator_groups();
|
||||||
|
let closed = subpath.closed();
|
||||||
|
bezpath_from_manipulator_groups(maniputor_groups, closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
|
||||||
|
let mut bezpath = kurbo::BezPath::new();
|
||||||
|
let mut out_handle;
|
||||||
|
|
||||||
|
let Some(first) = manipulator_groups.first() else { return bezpath };
|
||||||
|
bezpath.move_to(dvec2_to_point(first.anchor));
|
||||||
|
out_handle = first.out_handle;
|
||||||
|
|
||||||
|
for manipulator in manipulator_groups.iter().skip(1) {
|
||||||
|
match (out_handle, manipulator.in_handle) {
|
||||||
|
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
|
||||||
|
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
|
||||||
|
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||||
|
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||||
|
}
|
||||||
|
out_handle = manipulator.out_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if closed {
|
||||||
|
match (out_handle, first.in_handle) {
|
||||||
|
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
|
||||||
|
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
|
||||||
|
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||||
|
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||||
|
}
|
||||||
|
bezpath.close_path();
|
||||||
|
}
|
||||||
|
bezpath
|
||||||
|
}
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ impl VectorData {
|
|||||||
/// Returns the number of linear segments connected to the given point.
|
/// Returns the number of linear segments connected to the given point.
|
||||||
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
|
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
|
||||||
self.segment_bezier_iter()
|
self.segment_bezier_iter()
|
||||||
.filter(|(_, bez, start, end)| ((*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear)))
|
.filter(|(_, bez, start, end)| (*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear))
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ fn union<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, Vector
|
|||||||
// Reverse vector data so that the result style is the style of the first vector data
|
// Reverse vector data so that the result style is the style of the first vector data
|
||||||
let mut vector_data_reversed = vector_data.rev();
|
let mut vector_data_reversed = vector_data.rev();
|
||||||
|
|
||||||
let mut result_vector_data_table = VectorDataTable::default();
|
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data_reversed.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
||||||
result_vector_data_table.push(vector_data_reversed.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
|
||||||
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
||||||
|
|
||||||
// Loop over all vector data and union it with the result
|
// Loop over all vector data and union it with the result
|
||||||
@@ -113,8 +112,7 @@ fn union<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, Vector
|
|||||||
fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
|
fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
|
||||||
let mut vector_data = vector_data.into_iter();
|
let mut vector_data = vector_data.into_iter();
|
||||||
|
|
||||||
let mut result_vector_data_table = VectorDataTable::default();
|
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
||||||
result_vector_data_table.push(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
|
||||||
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
||||||
|
|
||||||
let mut next_vector_data = vector_data.next();
|
let mut next_vector_data = vector_data.next();
|
||||||
@@ -145,8 +143,7 @@ fn subtract<'a>(vector_data: impl Iterator<Item = InstanceRef<'a, VectorData>>)
|
|||||||
fn intersect<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
|
fn intersect<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, VectorData>>) -> VectorDataTable {
|
||||||
let mut vector_data = vector_data.rev();
|
let mut vector_data = vector_data.rev();
|
||||||
|
|
||||||
let mut result_vector_data_table = VectorDataTable::default();
|
let mut result_vector_data_table = VectorDataTable::new_instance(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
||||||
result_vector_data_table.push(vector_data.next().map(|x| x.to_instance_cloned()).unwrap_or_default());
|
|
||||||
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
let mut first_instance = result_vector_data_table.instance_mut_iter().next().expect("Expected the one instance we just pushed");
|
||||||
|
|
||||||
let default = Instance::default();
|
let default = Instance::default();
|
||||||
@@ -225,71 +222,67 @@ fn difference<'a>(vector_data: impl DoubleEndedIterator<Item = InstanceRef<'a, V
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> VectorDataTable {
|
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> VectorDataTable {
|
||||||
let mut result_table = VectorDataTable::default();
|
graphic_group_table
|
||||||
|
.instance_ref_iter()
|
||||||
|
.flat_map(|element| {
|
||||||
|
match element.instance.clone() {
|
||||||
|
GraphicElement::VectorData(vector_data) => {
|
||||||
|
// Apply the parent group's transform to each element of vector data
|
||||||
|
vector_data
|
||||||
|
.instance_iter()
|
||||||
|
.map(|mut sub_vector_data| {
|
||||||
|
sub_vector_data.transform = *element.transform * sub_vector_data.transform;
|
||||||
|
|
||||||
for element in graphic_group_table.instance_ref_iter() {
|
sub_vector_data
|
||||||
match element.instance.clone() {
|
})
|
||||||
GraphicElement::VectorData(vector_data) => {
|
.collect::<Vec<_>>()
|
||||||
// Apply the parent group's transform to each element of vector data
|
}
|
||||||
for mut sub_vector_data in vector_data.instance_iter() {
|
GraphicElement::RasterDataCPU(image) => {
|
||||||
sub_vector_data.transform = *element.transform * sub_vector_data.transform;
|
let make_instance = |transform| {
|
||||||
|
// Convert the image frame into a rectangular subpath with the image's transform
|
||||||
|
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||||
|
subpath.apply_transform(transform);
|
||||||
|
|
||||||
result_table.push(sub_vector_data);
|
// Create a vector data table row from the rectangular subpath, with a default black fill
|
||||||
|
let mut instance = VectorData::from_subpath(subpath);
|
||||||
|
instance.style.set_fill(Fill::Solid(Color::BLACK));
|
||||||
|
|
||||||
|
Instance { instance, ..Default::default() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply the parent group's transform to each element of raster data
|
||||||
|
image.instance_ref_iter().map(|instance| make_instance(*element.transform * *instance.transform)).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
GraphicElement::RasterDataGPU(image) => {
|
||||||
|
let make_instance = |transform| {
|
||||||
|
// Convert the image frame into a rectangular subpath with the image's transform
|
||||||
|
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||||
|
subpath.apply_transform(transform);
|
||||||
|
|
||||||
|
// Create a vector data table row from the rectangular subpath, with a default black fill
|
||||||
|
let mut instance = VectorData::from_subpath(subpath);
|
||||||
|
instance.style.set_fill(Fill::Solid(Color::BLACK));
|
||||||
|
|
||||||
|
Instance { instance, ..Default::default() }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply the parent group's transform to each element of raster data
|
||||||
|
image.instance_ref_iter().map(|instance| make_instance(*element.transform * *instance.transform)).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
GraphicElement::GraphicGroup(mut graphic_group) => {
|
||||||
|
// Apply the parent group's transform to each element of inner group
|
||||||
|
for sub_element in graphic_group.instance_mut_iter() {
|
||||||
|
*sub_element.transform = *element.transform * *sub_element.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively flatten the inner group into vector data
|
||||||
|
let unioned = boolean_operation_on_vector_data_table(flatten_vector_data(&graphic_group).instance_ref_iter(), BooleanOperation::Union);
|
||||||
|
|
||||||
|
unioned.instance_iter().collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphicElement::RasterDataCPU(image) => {
|
})
|
||||||
let make_instance = |transform| {
|
.collect()
|
||||||
// Convert the image frame into a rectangular subpath with the image's transform
|
|
||||||
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
|
||||||
subpath.apply_transform(transform);
|
|
||||||
|
|
||||||
// Create a vector data table row from the rectangular subpath, with a default black fill
|
|
||||||
let mut instance = VectorData::from_subpath(subpath);
|
|
||||||
instance.style.set_fill(Fill::Solid(Color::BLACK));
|
|
||||||
|
|
||||||
Instance { instance, ..Default::default() }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply the parent group's transform to each element of raster data
|
|
||||||
for instance in image.instance_ref_iter() {
|
|
||||||
result_table.push(make_instance(*element.transform * *instance.transform));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GraphicElement::RasterDataGPU(image) => {
|
|
||||||
let make_instance = |transform| {
|
|
||||||
// Convert the image frame into a rectangular subpath with the image's transform
|
|
||||||
let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
|
||||||
subpath.apply_transform(transform);
|
|
||||||
|
|
||||||
// Create a vector data table row from the rectangular subpath, with a default black fill
|
|
||||||
let mut instance = VectorData::from_subpath(subpath);
|
|
||||||
instance.style.set_fill(Fill::Solid(Color::BLACK));
|
|
||||||
|
|
||||||
Instance { instance, ..Default::default() }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply the parent group's transform to each element of raster data
|
|
||||||
for instance in image.instance_ref_iter() {
|
|
||||||
result_table.push(make_instance(*element.transform * *instance.transform));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GraphicElement::GraphicGroup(mut graphic_group) => {
|
|
||||||
// Apply the parent group's transform to each element of inner group
|
|
||||||
for sub_element in graphic_group.instance_mut_iter() {
|
|
||||||
*sub_element.transform = *element.transform * *sub_element.transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively flatten the inner group into vector data
|
|
||||||
let unioned = boolean_operation_on_vector_data_table(flatten_vector_data(&graphic_group).instance_ref_iter(), BooleanOperation::Union);
|
|
||||||
|
|
||||||
for element in unioned.instance_iter() {
|
|
||||||
result_table.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result_table
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_path(vector: &VectorData, transform: DAffine2) -> Vec<path_bool::PathSegment> {
|
fn to_path(vector: &VectorData, transform: DAffine2) -> Vec<path_bool::PathSegment> {
|
||||||
|
|||||||
@@ -217,26 +217,30 @@ impl ApplicationIo for WasmApplicationIo {
|
|||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
|
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
|
||||||
log::trace!("Spawning window");
|
todo!("winit api changed, calling create_window on EventLoop is deprecated");
|
||||||
|
|
||||||
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
// log::trace!("Spawning window");
|
||||||
use winit::platform::wayland::EventLoopBuilderExtWayland;
|
|
||||||
|
|
||||||
#[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
||||||
let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
|
// use winit::platform::wayland::EventLoopBuilderExtWayland;
|
||||||
#[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
|
|
||||||
let event_loop = winit::event_loop::EventLoop::new().unwrap();
|
|
||||||
|
|
||||||
let window = winit::window::WindowBuilder::new()
|
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
||||||
.with_title("Graphite")
|
// let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
|
||||||
.with_inner_size(winit::dpi::PhysicalSize::new(800, 600))
|
// #[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
|
||||||
.build(&event_loop)
|
// let event_loop = winit::event_loop::EventLoop::new().unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SurfaceHandle {
|
// let window = event_loop
|
||||||
window_id: SurfaceId(window.id().into()),
|
// .create_window(
|
||||||
surface: Arc::new(window),
|
// winit::window::WindowAttributes::default()
|
||||||
}
|
// .with_title("Graphite")
|
||||||
|
// .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)),
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
// SurfaceHandle {
|
||||||
|
// window_id: SurfaceId(window.id().into()),
|
||||||
|
// surface: Arc::new(window),
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_nanos(10));
|
std::thread::sleep(std::time::Duration::from_nanos(10));
|
||||||
device.poll(wgpu::Maintain::Poll);
|
device.poll(wgpu::PollType::Poll).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let executor = create_executor(proto_graph)?;
|
let executor = create_executor(proto_graph)?;
|
||||||
@@ -123,7 +123,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
println!("{:?}", result);
|
println!("{:?}", result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(16));
|
tokio::time::sleep(std::time::Duration::from_millis(16)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,26 @@ fn luminance<T: Adjust<Color>>(
|
|||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Raster"))]
|
||||||
|
fn gamma_correction<T: Adjust<Color>>(
|
||||||
|
_: impl Ctx,
|
||||||
|
#[implementations(
|
||||||
|
Color,
|
||||||
|
RasterDataTable<CPU>,
|
||||||
|
GradientStops,
|
||||||
|
)]
|
||||||
|
mut input: T,
|
||||||
|
#[default(2.2)]
|
||||||
|
#[range((0.01, 10.))]
|
||||||
|
#[hard_min(0.0001)]
|
||||||
|
gamma: f64,
|
||||||
|
inverse: bool,
|
||||||
|
) -> T {
|
||||||
|
let exponent = if inverse { 1. / gamma } else { gamma };
|
||||||
|
input.adjust(|color| color.gamma(exponent as f32));
|
||||||
|
input
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Raster: Channels"))]
|
#[node_macro::node(category("Raster: Channels"))]
|
||||||
fn extract_channel<T: Adjust<Color>>(
|
fn extract_channel<T: Adjust<Color>>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
|
|||||||
@@ -8,34 +8,33 @@ use std::cmp::{max, min};
|
|||||||
|
|
||||||
#[node_macro::node(category("Raster: Filter"))]
|
#[node_macro::node(category("Raster: Filter"))]
|
||||||
async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percentage) -> RasterDataTable<CPU> {
|
async fn dehaze(_: impl Ctx, image_frame: RasterDataTable<CPU>, strength: Percentage) -> RasterDataTable<CPU> {
|
||||||
let mut result_table = RasterDataTable::default();
|
image_frame
|
||||||
|
.instance_iter()
|
||||||
|
.map(|mut image_frame_instance| {
|
||||||
|
let image = image_frame_instance.instance;
|
||||||
|
// Prepare the image data for processing
|
||||||
|
let image_data = bytemuck::cast_vec(image.data.clone());
|
||||||
|
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
|
||||||
|
let dynamic_image: DynamicImage = image_buffer.into();
|
||||||
|
|
||||||
for mut image_frame_instance in image_frame.instance_iter() {
|
// Run the dehaze algorithm
|
||||||
let image = image_frame_instance.instance;
|
let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.);
|
||||||
// Prepare the image data for processing
|
|
||||||
let image_data = bytemuck::cast_vec(image.data.clone());
|
|
||||||
let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type.");
|
|
||||||
let dynamic_image: DynamicImage = image_buffer.into();
|
|
||||||
|
|
||||||
// Run the dehaze algorithm
|
// Prepare the image data for returning
|
||||||
let dehazed_dynamic_image = dehaze_image(dynamic_image, strength / 100.);
|
let buffer = dehazed_dynamic_image.to_rgba32f().into_raw();
|
||||||
|
let color_vec = bytemuck::cast_vec(buffer);
|
||||||
|
let dehazed_image = Image {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
data: color_vec,
|
||||||
|
base64_string: None,
|
||||||
|
};
|
||||||
|
|
||||||
// Prepare the image data for returning
|
image_frame_instance.instance = Raster::new_cpu(dehazed_image);
|
||||||
let buffer = dehazed_dynamic_image.to_rgba32f().into_raw();
|
image_frame_instance.source_node_id = None;
|
||||||
let color_vec = bytemuck::cast_vec(buffer);
|
image_frame_instance
|
||||||
let dehazed_image = Image {
|
})
|
||||||
width: image.width,
|
.collect()
|
||||||
height: image.height,
|
|
||||||
data: color_vec,
|
|
||||||
base64_string: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
image_frame_instance.instance = Raster::new_cpu(dehazed_image);
|
|
||||||
image_frame_instance.source_node_id = None;
|
|
||||||
result_table.push(image_frame_instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
result_table
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is no real point in modifying these values because they do not change the final result all that much.
|
// There is no real point in modifying these values because they do not change the final result all that much.
|
||||||
|
|||||||
@@ -20,27 +20,26 @@ async fn blur(
|
|||||||
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
|
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
|
||||||
gamma: bool,
|
gamma: bool,
|
||||||
) -> RasterDataTable<CPU> {
|
) -> RasterDataTable<CPU> {
|
||||||
let mut result_table = RasterDataTable::default();
|
image_frame
|
||||||
|
.instance_iter()
|
||||||
|
.map(|mut image_instance| {
|
||||||
|
let image = image_instance.instance.clone();
|
||||||
|
|
||||||
for mut image_instance in image_frame.instance_iter() {
|
// Run blur algorithm
|
||||||
let image = image_instance.instance.clone();
|
let blurred_image = if radius < 0.1 {
|
||||||
|
// Minimum blur radius
|
||||||
|
image.clone()
|
||||||
|
} else if box_blur {
|
||||||
|
Raster::new_cpu(box_blur_algorithm(image.into_data(), radius, gamma))
|
||||||
|
} else {
|
||||||
|
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
|
||||||
|
};
|
||||||
|
|
||||||
// Run blur algorithm
|
image_instance.instance = blurred_image;
|
||||||
let blurred_image = if radius < 0.1 {
|
image_instance.source_node_id = None;
|
||||||
// Minimum blur radius
|
image_instance
|
||||||
image.clone()
|
})
|
||||||
} else if box_blur {
|
.collect()
|
||||||
Raster::new_cpu(box_blur_algorithm(image.into_data(), radius, gamma))
|
|
||||||
} else {
|
|
||||||
Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma))
|
|
||||||
};
|
|
||||||
|
|
||||||
image_instance.instance = blurred_image;
|
|
||||||
image_instance.source_node_id = None;
|
|
||||||
result_table.push(image_instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
result_table
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1D gaussian kernel
|
// 1D gaussian kernel
|
||||||
|
|||||||
@@ -31,69 +31,68 @@ impl From<std::io::Error> for Error {
|
|||||||
|
|
||||||
#[node_macro::node(category("Debug: Raster"))]
|
#[node_macro::node(category("Debug: Raster"))]
|
||||||
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
|
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: RasterDataTable<CPU>) -> RasterDataTable<CPU> {
|
||||||
let mut result_table = RasterDataTable::default();
|
image_frame
|
||||||
|
.instance_iter()
|
||||||
|
.filter_map(|mut image_frame_instance| {
|
||||||
|
let image_frame_transform = image_frame_instance.transform;
|
||||||
|
let image = image_frame_instance.instance;
|
||||||
|
|
||||||
for mut image_frame_instance in image_frame.instance_iter() {
|
// Resize the image using the image crate
|
||||||
let image_frame_transform = image_frame_instance.transform;
|
let data = bytemuck::cast_vec(image.data.clone());
|
||||||
let image = image_frame_instance.instance;
|
|
||||||
|
|
||||||
// Resize the image using the image crate
|
let footprint = ctx.footprint();
|
||||||
let data = bytemuck::cast_vec(image.data.clone());
|
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
||||||
|
let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
|
||||||
|
let intersection = viewport_bounds.intersect(&image_bounds);
|
||||||
|
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
|
||||||
|
let size = intersection.size();
|
||||||
|
let size_px = image_size.transform_vector2(size).as_uvec2();
|
||||||
|
|
||||||
let footprint = ctx.footprint();
|
// If the image would not be visible, add nothing.
|
||||||
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
if size.x <= 0. || size.y <= 0. {
|
||||||
let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
|
return None;
|
||||||
let intersection = viewport_bounds.intersect(&image_bounds);
|
}
|
||||||
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
|
|
||||||
let size = intersection.size();
|
|
||||||
let size_px = image_size.transform_vector2(size).as_uvec2();
|
|
||||||
|
|
||||||
// If the image would not be visible, add nothing.
|
let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
|
||||||
if size.x <= 0. || size.y <= 0. {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
|
let dynamic_image: ::image::DynamicImage = image_buffer.into();
|
||||||
|
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
|
||||||
|
let offset_px = image_size.transform_vector2(offset).as_uvec2();
|
||||||
|
let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y);
|
||||||
|
|
||||||
let dynamic_image: ::image::DynamicImage = image_buffer.into();
|
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
|
||||||
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
|
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
|
||||||
let offset_px = image_size.transform_vector2(offset).as_uvec2();
|
let mut new_width = size_px.x;
|
||||||
let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y);
|
let mut new_height = size_px.y;
|
||||||
|
|
||||||
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
|
// Only downscale the image for now
|
||||||
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
|
let resized = if new_width < image.width || new_height < image.height {
|
||||||
let mut new_width = size_px.x;
|
new_width = viewport_resolution_x as u32;
|
||||||
let mut new_height = size_px.y;
|
new_height = viewport_resolution_y as u32;
|
||||||
|
// TODO: choose filter based on quality requirements
|
||||||
|
cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle)
|
||||||
|
} else {
|
||||||
|
cropped
|
||||||
|
};
|
||||||
|
let buffer = resized.to_rgba32f();
|
||||||
|
let buffer = buffer.into_raw();
|
||||||
|
let vec = bytemuck::cast_vec(buffer);
|
||||||
|
let image = Image {
|
||||||
|
width: new_width,
|
||||||
|
height: new_height,
|
||||||
|
data: vec,
|
||||||
|
base64_string: None,
|
||||||
|
};
|
||||||
|
// we need to adjust the offset if we truncate the offset calculation
|
||||||
|
|
||||||
// Only downscale the image for now
|
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||||
let resized = if new_width < image.width || new_height < image.height {
|
|
||||||
new_width = viewport_resolution_x as u32;
|
|
||||||
new_height = viewport_resolution_y as u32;
|
|
||||||
// TODO: choose filter based on quality requirements
|
|
||||||
cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle)
|
|
||||||
} else {
|
|
||||||
cropped
|
|
||||||
};
|
|
||||||
let buffer = resized.to_rgba32f();
|
|
||||||
let buffer = buffer.into_raw();
|
|
||||||
let vec = bytemuck::cast_vec(buffer);
|
|
||||||
let image = Image {
|
|
||||||
width: new_width,
|
|
||||||
height: new_height,
|
|
||||||
data: vec,
|
|
||||||
base64_string: None,
|
|
||||||
};
|
|
||||||
// we need to adjust the offset if we truncate the offset calculation
|
|
||||||
|
|
||||||
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
image_frame_instance.transform = new_transform;
|
||||||
|
image_frame_instance.source_node_id = None;
|
||||||
image_frame_instance.transform = new_transform;
|
image_frame_instance.instance = Raster::new_cpu(image);
|
||||||
image_frame_instance.source_node_id = None;
|
Some(image_frame_instance)
|
||||||
image_frame_instance.instance = Raster::new_cpu(image);
|
})
|
||||||
result_table.push(image_frame_instance)
|
.collect()
|
||||||
}
|
|
||||||
|
|
||||||
result_table
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Raster: Channels"))]
|
#[node_macro::node(category("Raster: Channels"))]
|
||||||
@@ -105,84 +104,85 @@ pub fn combine_channels(
|
|||||||
#[expose] blue: RasterDataTable<CPU>,
|
#[expose] blue: RasterDataTable<CPU>,
|
||||||
#[expose] alpha: RasterDataTable<CPU>,
|
#[expose] alpha: RasterDataTable<CPU>,
|
||||||
) -> RasterDataTable<CPU> {
|
) -> RasterDataTable<CPU> {
|
||||||
let mut result_table = RasterDataTable::default();
|
|
||||||
|
|
||||||
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
|
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
|
||||||
let red = red.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
let red = red.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||||
let green = green.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
let green = green.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||||
let blue = blue.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
let blue = blue.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||||
let alpha = alpha.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
let alpha = alpha.instance_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||||
|
|
||||||
for (((red, green), blue), alpha) in red.zip(green).zip(blue).zip(alpha) {
|
red.zip(green)
|
||||||
// Turn any default zero-sized image instances into None
|
.zip(blue)
|
||||||
let red = red.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
.zip(alpha)
|
||||||
let green = green.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
.filter_map(|(((red, green), blue), alpha)| {
|
||||||
let blue = blue.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
// Turn any default zero-sized image instances into None
|
||||||
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
let red = red.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
||||||
|
let green = green.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
||||||
|
let blue = blue.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
||||||
|
let alpha = alpha.filter(|i| i.instance.width > 0 && i.instance.height > 0);
|
||||||
|
|
||||||
// Get this instance's transform and alpha blending mode from the first non-empty channel
|
// Get this instance's transform and alpha blending mode from the first non-empty channel
|
||||||
let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else {
|
let Some((transform, alpha_blending)) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| (i.transform, i.alpha_blending)) else {
|
||||||
continue;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the common width and height of the channels, which must have equal dimensions
|
// Get the common width and height of the channels, which must have equal dimensions
|
||||||
let channel_dimensions = [
|
let channel_dimensions = [
|
||||||
red.as_ref().map(|r| (r.instance.width, r.instance.height)),
|
red.as_ref().map(|r| (r.instance.width, r.instance.height)),
|
||||||
green.as_ref().map(|g| (g.instance.width, g.instance.height)),
|
green.as_ref().map(|g| (g.instance.width, g.instance.height)),
|
||||||
blue.as_ref().map(|b| (b.instance.width, b.instance.height)),
|
blue.as_ref().map(|b| (b.instance.width, b.instance.height)),
|
||||||
alpha.as_ref().map(|a| (a.instance.width, a.instance.height)),
|
alpha.as_ref().map(|a| (a.instance.width, a.instance.height)),
|
||||||
];
|
];
|
||||||
if channel_dimensions.iter().all(Option::is_none)
|
if channel_dimensions.iter().all(Option::is_none)
|
||||||
|| channel_dimensions
|
|| channel_dimensions
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y))
|
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y))
|
||||||
{
|
{
|
||||||
continue;
|
return None;
|
||||||
}
|
}
|
||||||
let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else { continue };
|
let Some(&(width, height)) = channel_dimensions.iter().flatten().next() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
// Create a new image for this instance output
|
// Create a new image for this instance output
|
||||||
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
||||||
|
|
||||||
// Iterate over all pixels in the image and set the color channels
|
// Iterate over all pixels in the image and set the color channels
|
||||||
for y in 0..image.height() {
|
for y in 0..image.height() {
|
||||||
for x in 0..image.width() {
|
for x in 0..image.width() {
|
||||||
let image_pixel = image.get_pixel_mut(x, y).unwrap();
|
let image_pixel = image.get_pixel_mut(x, y).unwrap();
|
||||||
|
|
||||||
if let Some(r) = red.as_ref().and_then(|r| r.instance.get_pixel(x, y)) {
|
if let Some(r) = red.as_ref().and_then(|r| r.instance.get_pixel(x, y)) {
|
||||||
image_pixel.set_red(r.l().cast_linear_channel());
|
image_pixel.set_red(r.l().cast_linear_channel());
|
||||||
} else {
|
} else {
|
||||||
image_pixel.set_red(Channel::from_linear(0.));
|
image_pixel.set_red(Channel::from_linear(0.));
|
||||||
}
|
}
|
||||||
if let Some(g) = green.as_ref().and_then(|g| g.instance.get_pixel(x, y)) {
|
if let Some(g) = green.as_ref().and_then(|g| g.instance.get_pixel(x, y)) {
|
||||||
image_pixel.set_green(g.l().cast_linear_channel());
|
image_pixel.set_green(g.l().cast_linear_channel());
|
||||||
} else {
|
} else {
|
||||||
image_pixel.set_green(Channel::from_linear(0.));
|
image_pixel.set_green(Channel::from_linear(0.));
|
||||||
}
|
}
|
||||||
if let Some(b) = blue.as_ref().and_then(|b| b.instance.get_pixel(x, y)) {
|
if let Some(b) = blue.as_ref().and_then(|b| b.instance.get_pixel(x, y)) {
|
||||||
image_pixel.set_blue(b.l().cast_linear_channel());
|
image_pixel.set_blue(b.l().cast_linear_channel());
|
||||||
} else {
|
} else {
|
||||||
image_pixel.set_blue(Channel::from_linear(0.));
|
image_pixel.set_blue(Channel::from_linear(0.));
|
||||||
}
|
}
|
||||||
if let Some(a) = alpha.as_ref().and_then(|a| a.instance.get_pixel(x, y)) {
|
if let Some(a) = alpha.as_ref().and_then(|a| a.instance.get_pixel(x, y)) {
|
||||||
image_pixel.set_alpha(a.l().cast_linear_channel());
|
image_pixel.set_alpha(a.l().cast_linear_channel());
|
||||||
} else {
|
} else {
|
||||||
image_pixel.set_alpha(Channel::from_linear(1.));
|
image_pixel.set_alpha(Channel::from_linear(1.));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add this instance to the result table
|
Some(Instance {
|
||||||
result_table.push(Instance {
|
instance: Raster::new_cpu(image),
|
||||||
instance: Raster::new_cpu(image),
|
transform,
|
||||||
transform,
|
alpha_blending,
|
||||||
alpha_blending,
|
source_node_id: None,
|
||||||
source_node_id: None,
|
})
|
||||||
});
|
})
|
||||||
}
|
.collect()
|
||||||
|
|
||||||
result_table
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Raster"))]
|
#[node_macro::node(category("Raster"))]
|
||||||
@@ -201,91 +201,85 @@ pub fn mask(
|
|||||||
};
|
};
|
||||||
let stencil_size = DVec2::new(stencil_instance.instance.width as f64, stencil_instance.instance.height as f64);
|
let stencil_size = DVec2::new(stencil_instance.instance.width as f64, stencil_instance.instance.height as f64);
|
||||||
|
|
||||||
let mut result_table = RasterDataTable::default();
|
image
|
||||||
|
.instance_iter()
|
||||||
|
.filter_map(|mut image_instance| {
|
||||||
|
let image_size = DVec2::new(image_instance.instance.width as f64, image_instance.instance.height as f64);
|
||||||
|
let mask_size = stencil_instance.transform.decompose_scale();
|
||||||
|
|
||||||
for mut image_instance in image.instance_iter() {
|
if mask_size == DVec2::ZERO {
|
||||||
let image_size = DVec2::new(image_instance.instance.width as f64, image_instance.instance.height as f64);
|
return None;
|
||||||
let mask_size = stencil_instance.transform.decompose_scale();
|
|
||||||
|
|
||||||
if mask_size == DVec2::ZERO {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transforms a point from the background image to the foreground image
|
|
||||||
let bg_to_fg = image_instance.transform * DAffine2::from_scale(1. / image_size);
|
|
||||||
let stencil_transform_inverse = stencil_instance.transform.inverse();
|
|
||||||
|
|
||||||
for y in 0..image_instance.instance.height {
|
|
||||||
for x in 0..image_instance.instance.width {
|
|
||||||
let image_point = DVec2::new(x as f64, y as f64);
|
|
||||||
let mask_point = bg_to_fg.transform_point2(image_point);
|
|
||||||
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
|
|
||||||
let mask_point = stencil_instance.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
|
|
||||||
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_instance.transform.inverse()).transform_point2(mask_point);
|
|
||||||
|
|
||||||
let image_pixel = image_instance.instance.data_mut().get_pixel_mut(x, y).unwrap();
|
|
||||||
let mask_pixel = stencil_instance.instance.sample(mask_point);
|
|
||||||
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
result_table.push(image_instance);
|
// Transforms a point from the background image to the foreground image
|
||||||
}
|
let bg_to_fg = image_instance.transform * DAffine2::from_scale(1. / image_size);
|
||||||
|
let stencil_transform_inverse = stencil_instance.transform.inverse();
|
||||||
|
|
||||||
result_table
|
for y in 0..image_instance.instance.height {
|
||||||
|
for x in 0..image_instance.instance.width {
|
||||||
|
let image_point = DVec2::new(x as f64, y as f64);
|
||||||
|
let mask_point = bg_to_fg.transform_point2(image_point);
|
||||||
|
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
|
||||||
|
let mask_point = stencil_instance.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
|
||||||
|
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_instance.transform.inverse()).transform_point2(mask_point);
|
||||||
|
|
||||||
|
let image_pixel = image_instance.instance.data_mut().get_pixel_mut(x, y).unwrap();
|
||||||
|
let mask_pixel = stencil_instance.instance.sample(mask_point);
|
||||||
|
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(image_instance)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
|
pub fn extend_image_to_bounds(_: impl Ctx, image: RasterDataTable<CPU>, bounds: DAffine2) -> RasterDataTable<CPU> {
|
||||||
let mut result_table = RasterDataTable::default();
|
image
|
||||||
|
.instance_iter()
|
||||||
for mut image_instance in image.instance_iter() {
|
.map(|mut image_instance| {
|
||||||
let image_aabb = Bbox::unit().affine_transform(image_instance.transform).to_axis_aligned_bbox();
|
let image_aabb = Bbox::unit().affine_transform(image_instance.transform).to_axis_aligned_bbox();
|
||||||
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
|
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
|
||||||
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
|
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {
|
||||||
result_table.push(image_instance);
|
return image_instance;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image_data = &image_instance.instance.data;
|
|
||||||
let (image_width, image_height) = (image_instance.instance.width, image_instance.instance.height);
|
|
||||||
if image_width == 0 || image_height == 0 {
|
|
||||||
for image_instance in empty_image((), bounds, Color::TRANSPARENT).instance_iter() {
|
|
||||||
result_table.push(image_instance);
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
|
let image_data = &image_instance.instance.data;
|
||||||
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image_instance.transform.inverse();
|
let (image_width, image_height) = (image_instance.instance.width, image_instance.instance.height);
|
||||||
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
|
if image_width == 0 || image_height == 0 {
|
||||||
|
return empty_image((), bounds, Color::TRANSPARENT).instance_iter().next().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
|
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
|
||||||
let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale);
|
let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * image_instance.transform.inverse();
|
||||||
let new_scale = new_end - new_start;
|
let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox();
|
||||||
|
|
||||||
// Copy over original image into enlarged image.
|
let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO);
|
||||||
let mut new_image = Image::new(new_scale.x as u32, new_scale.y as u32, Color::TRANSPARENT);
|
let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale);
|
||||||
let offset_in_new_image = (-new_start).as_uvec2();
|
let new_scale = new_end - new_start;
|
||||||
for y in 0..image_height {
|
|
||||||
let old_start = y * image_width;
|
|
||||||
let new_start = (y + offset_in_new_image.y) * new_image.width + offset_in_new_image.x;
|
|
||||||
let old_row = &image_data[old_start as usize..(old_start + image_width) as usize];
|
|
||||||
let new_row = &mut new_image.data[new_start as usize..(new_start + image_width) as usize];
|
|
||||||
new_row.copy_from_slice(old_row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute new transform.
|
// Copy over original image into enlarged image.
|
||||||
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
|
let mut new_image = Image::new(new_scale.x as u32, new_scale.y as u32, Color::TRANSPARENT);
|
||||||
let new_texture_to_layer_space = image_instance.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
|
let offset_in_new_image = (-new_start).as_uvec2();
|
||||||
|
for y in 0..image_height {
|
||||||
|
let old_start = y * image_width;
|
||||||
|
let new_start = (y + offset_in_new_image.y) * new_image.width + offset_in_new_image.x;
|
||||||
|
let old_row = &image_data[old_start as usize..(old_start + image_width) as usize];
|
||||||
|
let new_row = &mut new_image.data[new_start as usize..(new_start + image_width) as usize];
|
||||||
|
new_row.copy_from_slice(old_row);
|
||||||
|
}
|
||||||
|
|
||||||
image_instance.instance = Raster::new_cpu(new_image);
|
// Compute new transform.
|
||||||
image_instance.transform = new_texture_to_layer_space;
|
// let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse();
|
||||||
image_instance.source_node_id = None;
|
let new_texture_to_layer_space = image_instance.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale);
|
||||||
result_table.push(image_instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
result_table
|
image_instance.instance = Raster::new_cpu(new_image);
|
||||||
|
image_instance.transform = new_texture_to_layer_space;
|
||||||
|
image_instance.source_node_id = None;
|
||||||
|
image_instance
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Debug: Raster"))]
|
#[node_macro::node(category("Debug: Raster"))]
|
||||||
@@ -392,14 +386,11 @@ pub fn noise_pattern(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = RasterDataTable::default();
|
return RasterDataTable::new_instance(Instance {
|
||||||
result.push(Instance {
|
|
||||||
instance: Raster::new_cpu(image),
|
instance: Raster::new_cpu(image),
|
||||||
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
noise.set_noise_type(Some(noise_type));
|
noise.set_noise_type(Some(noise_type));
|
||||||
@@ -457,14 +448,11 @@ pub fn noise_pattern(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = RasterDataTable::default();
|
RasterDataTable::new_instance(Instance {
|
||||||
result.push(Instance {
|
|
||||||
instance: Raster::new_cpu(image),
|
instance: Raster::new_cpu(image),
|
||||||
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
})
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Raster: Pattern"))]
|
#[node_macro::node(category("Raster: Pattern"))]
|
||||||
@@ -502,20 +490,16 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> RasterDataTable<CPU> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let image = Image {
|
RasterDataTable::new_instance(Instance {
|
||||||
width,
|
instance: Raster::new_cpu(Image {
|
||||||
height,
|
width,
|
||||||
data,
|
height,
|
||||||
..Default::default()
|
data,
|
||||||
};
|
..Default::default()
|
||||||
let mut result = RasterDataTable::default();
|
}),
|
||||||
result.push(Instance {
|
|
||||||
instance: Raster::new_cpu(image),
|
|
||||||
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
})
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use graphene_core::Ctx;
|
|
||||||
|
|
||||||
#[node_macro::node(category("Web Request"))]
|
|
||||||
async fn get_request(_: impl Ctx, url: String) -> reqwest::Response {
|
|
||||||
reqwest::get(url).await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[node_macro::node(category("Web Request"))]
|
|
||||||
async fn post_request(_: impl Ctx, url: String, body: String) -> reqwest::Response {
|
|
||||||
reqwest::Client::new().post(url).body(body).send().await.unwrap()
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod any;
|
pub mod any;
|
||||||
pub mod http;
|
|
||||||
pub mod text;
|
pub mod text;
|
||||||
#[cfg(feature = "wasm")]
|
#[cfg(feature = "wasm")]
|
||||||
pub mod wasm_application_io;
|
pub mod wasm_application_io;
|
||||||
|
|||||||
@@ -59,6 +59,81 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<W
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
#[node_macro::node(category("Web Request"))]
|
||||||
|
async fn get_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, discard_result: bool) -> String {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
if discard_result {
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let _ = reqwest::get(url).await;
|
||||||
|
});
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
if discard_result {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = reqwest::get(url).await;
|
||||||
|
});
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tokio"))]
|
||||||
|
if discard_result {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(response) = reqwest::get(url).await else { return String::new() };
|
||||||
|
response.text().await.ok().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Web Request"))]
|
||||||
|
async fn post_request(_: impl Ctx, _primary: (), #[name("URL")] url: String, body: Vec<u8>, discard_result: bool) -> String {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
if discard_result {
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let _ = reqwest::Client::new().post(url).body(body).header("Content-Type", "application/octet-stream").send().await;
|
||||||
|
});
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
if discard_result {
|
||||||
|
let url = url.clone();
|
||||||
|
let body = body.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = reqwest::Client::new().post(url).body(body).header("Content-Type", "application/octet-stream").send().await;
|
||||||
|
});
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tokio"))]
|
||||||
|
if discard_result {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(response) = reqwest::Client::new().post(url).body(body).header("Content-Type", "application/octet-stream").send().await else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
response.text().await.ok().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Web Request"), name("String to Bytes"))]
|
||||||
|
fn string_to_bytes(_: impl Ctx, string: String) -> Vec<u8> {
|
||||||
|
string.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
||||||
|
fn image_to_bytes(_: impl Ctx, image: RasterDataTable<CPU>) -> Vec<u8> {
|
||||||
|
let Some(image) = image.instance_ref_iter().next() else { return vec![] };
|
||||||
|
image.instance.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::<Vec<u8>>()
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Web Request"))]
|
#[node_macro::node(category("Web Request"))]
|
||||||
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
|
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
|
||||||
let Some(api) = editor.application_io.as_ref() else {
|
let Some(api) = editor.application_io.as_ref() else {
|
||||||
@@ -145,7 +220,7 @@ async fn render_canvas(
|
|||||||
if !data.contains_artboard() && !render_config.hide_artboards {
|
if !data.contains_artboard() && !render_config.hide_artboards {
|
||||||
background = Color::WHITE;
|
background = Color::WHITE;
|
||||||
}
|
}
|
||||||
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y, &context, background)
|
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, &context, background)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to render Vello scene");
|
.expect("Failed to render Vello scene");
|
||||||
|
|
||||||
@@ -217,15 +292,12 @@ where
|
|||||||
|
|
||||||
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
|
let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap();
|
||||||
|
|
||||||
let mut result = RasterDataTable::default();
|
|
||||||
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
|
let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32);
|
||||||
result.push(Instance {
|
RasterDataTable::new_instance(Instance {
|
||||||
instance: Raster::new_cpu(image),
|
instance: Raster::new_cpu(image),
|
||||||
transform: footprint.transform,
|
transform: footprint.transform,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
})
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ impl MaskType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_defs(self, svg_defs: &mut String, uuid: u64, svg_string: String) {
|
fn write_to_defs(self, svg_defs: &mut String, uuid: u64, svg_string: String) {
|
||||||
let id = format!("mask-{}", uuid);
|
let id = format!("mask-{uuid}");
|
||||||
match self {
|
match self {
|
||||||
Self::Clip => write!(svg_defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg_string).unwrap(),
|
Self::Clip => write!(svg_defs, r##"<clipPath id="{id}">{svg_string}</clipPath>"##).unwrap(),
|
||||||
Self::Mask => write!(svg_defs, r##"<mask id="{id}" mask-type="alpha">{}</mask>"##, svg_string).unwrap(),
|
Self::Mask => write!(svg_defs, r##"<mask id="{id}" mask-type="alpha">{svg_string}</mask>"##).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,9 +89,9 @@ impl SvgRender {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let matrix = format_transform_matrix(transform);
|
let matrix = format_transform_matrix(transform);
|
||||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
|
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
|
||||||
|
|
||||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g{transform}>"#, view_box);
|
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||||
self.svg.insert(0, svg_header.into());
|
self.svg.insert(0, svg_header.into());
|
||||||
self.svg.push("</g></svg>".into());
|
self.svg.push("</g></svg>".into());
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ impl Default for SvgRender {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct RenderContext {
|
pub struct RenderContext {
|
||||||
#[cfg(feature = "vello")]
|
#[cfg(feature = "vello")]
|
||||||
pub resource_overrides: HashMap<u64, std::sync::Arc<wgpu::Texture>>,
|
pub resource_overrides: HashMap<u64, wgpu::Texture>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static state used whilst rendering
|
/// Static state used whilst rendering
|
||||||
@@ -267,7 +267,7 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||||||
mask_state = None;
|
mask_state = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = format!("mask-{}", uuid);
|
let id = format!("mask-{uuid}");
|
||||||
let selector = format!("url(#{id})");
|
let selector = format!("url(#{id})");
|
||||||
|
|
||||||
attributes.push(mask_type.to_attribute(), selector);
|
attributes.push(mask_type.to_attribute(), selector);
|
||||||
@@ -444,18 +444,18 @@ impl GraphicElementRendered for VectorDataTable {
|
|||||||
let can_use_order = !instance.instance.style.fill().is_none() && mask_type == MaskType::Mask;
|
let can_use_order = !instance.instance.style.fill().is_none() && mask_type == MaskType::Mask;
|
||||||
if !can_use_order {
|
if !can_use_order {
|
||||||
let id = format!("alignment-{}", generate_uuid());
|
let id = format!("alignment-{}", generate_uuid());
|
||||||
let mut vector_row = VectorDataTable::default();
|
|
||||||
let mut fill_instance = instance.instance.clone();
|
|
||||||
|
|
||||||
|
let mut fill_instance = instance.instance.clone();
|
||||||
fill_instance.style.clear_stroke();
|
fill_instance.style.clear_stroke();
|
||||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||||
|
|
||||||
vector_row.push(Instance {
|
let vector_row = VectorDataTable::new_instance(Instance {
|
||||||
instance: fill_instance,
|
instance: fill_instance,
|
||||||
alpha_blending: *instance.alpha_blending,
|
alpha_blending: *instance.alpha_blending,
|
||||||
transform: *instance.transform,
|
transform: *instance.transform,
|
||||||
source_node_id: None,
|
source_node_id: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
push_id = Some((id, mask_type, vector_row));
|
push_id = Some((id, mask_type, vector_row));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,7 +477,7 @@ impl GraphicElementRendered for VectorDataTable {
|
|||||||
let (x, y) = quad.top_left().into();
|
let (x, y) = quad.top_left().into();
|
||||||
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
||||||
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
|
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
|
||||||
let rect = format!(r##"<rect x="{}" y="{}" width="{width}" height="{height}" fill="white" />"##, x, y);
|
let rect = format!(r##"<rect x="{x}" y="{y}" width="{width}" height="{height}" fill="white" />"##);
|
||||||
match mask_type {
|
match mask_type {
|
||||||
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
|
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
|
||||||
MaskType::Mask => write!(defs, r##"<mask id="{id}">{}{}</mask>"##, rect, svg.svg.to_svg_string()).unwrap(),
|
MaskType::Mask => write!(defs, r##"<mask id="{id}">{}{}</mask>"##, rect, svg.svg.to_svg_string()).unwrap(),
|
||||||
@@ -564,13 +564,11 @@ impl GraphicElementRendered for VectorDataTable {
|
|||||||
.stroke()
|
.stroke()
|
||||||
.is_some_and(|stroke| stroke.align == StrokeAlign::Outside && !instance.instance.style.fill().is_none());
|
.is_some_and(|stroke| stroke.align == StrokeAlign::Outside && !instance.instance.style.fill().is_none());
|
||||||
if can_draw_aligned_stroke && !reorder_for_outside {
|
if can_draw_aligned_stroke && !reorder_for_outside {
|
||||||
let mut vector_data = VectorDataTable::default();
|
|
||||||
|
|
||||||
let mut fill_instance = instance.instance.clone();
|
let mut fill_instance = instance.instance.clone();
|
||||||
fill_instance.style.clear_stroke();
|
fill_instance.style.clear_stroke();
|
||||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||||
|
|
||||||
vector_data.push(Instance {
|
let vector_data = VectorDataTable::new_instance(Instance {
|
||||||
instance: fill_instance,
|
instance: fill_instance,
|
||||||
alpha_blending: *instance.alpha_blending,
|
alpha_blending: *instance.alpha_blending,
|
||||||
transform: *instance.transform,
|
transform: *instance.transform,
|
||||||
@@ -639,7 +637,11 @@ impl GraphicElementRendered for VectorDataTable {
|
|||||||
let bounds = instance.instance.nonzero_bounding_box();
|
let bounds = instance.instance.nonzero_bounding_box();
|
||||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||||
|
|
||||||
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_default();
|
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 mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||||
|
|
||||||
let start = mod_points.transform_point2(gradient.start);
|
let start = mod_points.transform_point2(gradient.start);
|
||||||
@@ -666,7 +668,11 @@ impl GraphicElementRendered for VectorDataTable {
|
|||||||
});
|
});
|
||||||
// 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.
|
// 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.
|
// 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 = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_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());
|
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);
|
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||||
}
|
}
|
||||||
@@ -983,7 +989,7 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
|||||||
if image.data.is_empty() {
|
if image.data.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let image = peniko::Image::new(image.to_flat_u8().0.into(), peniko::Format::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat);
|
let image = peniko::Image::new(image.to_flat_u8().0.into(), peniko::ImageFormat::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat);
|
||||||
let transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
|
let transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
|
||||||
|
|
||||||
scene.draw_image(&image, kurbo::Affine::new(transform.to_cols_array()));
|
scene.draw_image(&image, kurbo::Affine::new(transform.to_cols_array()));
|
||||||
@@ -1035,10 +1041,10 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for instance in self.instance_ref_iter() {
|
for instance in self.instance_ref_iter() {
|
||||||
let image = peniko::Image::new(vec![].into(), peniko::Format::Rgba8, instance.instance.data().width(), instance.instance.data().height()).with_extend(peniko::Extend::Repeat);
|
let image = peniko::Image::new(vec![].into(), peniko::ImageFormat::Rgba8, instance.instance.data().width(), instance.instance.data().height()).with_extend(peniko::Extend::Repeat);
|
||||||
|
|
||||||
let id = image.data.id();
|
let id = image.data.id();
|
||||||
context.resource_overrides.insert(id, instance.instance.data_owned());
|
context.resource_overrides.insert(id, instance.instance.data().clone());
|
||||||
|
|
||||||
render_stuff(image, *instance.transform, *instance.alpha_blending);
|
render_stuff(image, *instance.transform, *instance.alpha_blending);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "node-macro"
|
name = "node-macro"
|
||||||
publish = false
|
publish = false
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.88"
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
readme = "../../README.md"
|
readme = "../../README.md"
|
||||||
@@ -26,4 +26,3 @@ proc-macro-error2 = "2"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
graphene-core = { workspace = true }
|
graphene-core = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ impl Context {
|
|||||||
backends: wgpu::Backends::all(),
|
backends: wgpu::Backends::all(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let instance = Instance::new(instance_descriptor);
|
let instance = Instance::new(&instance_descriptor);
|
||||||
|
|
||||||
let adapter_options = wgpu::RequestAdapterOptions {
|
let adapter_options = wgpu::RequestAdapterOptions {
|
||||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
@@ -24,26 +24,24 @@ impl Context {
|
|||||||
force_fallback_adapter: false,
|
force_fallback_adapter: false,
|
||||||
};
|
};
|
||||||
// `request_adapter` instantiates the general connection to the GPU
|
// `request_adapter` instantiates the general connection to the GPU
|
||||||
let adapter = instance.request_adapter(&adapter_options).await?;
|
let adapter = instance.request_adapter(&adapter_options).await.ok()?;
|
||||||
|
|
||||||
let required_limits = adapter.limits();
|
let required_limits = adapter.limits();
|
||||||
// `request_device` instantiates the feature specific connection to the GPU, defining some parameters,
|
// `request_device` instantiates the feature specific connection to the GPU, defining some parameters,
|
||||||
// `features` being the available features.
|
// `features` being the available features.
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
.request_device(
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
&wgpu::DeviceDescriptor {
|
label: None,
|
||||||
label: None,
|
// #[cfg(not(feature = "passthrough"))]
|
||||||
// #[cfg(not(feature = "passthrough"))]
|
required_features: wgpu::Features::empty(),
|
||||||
required_features: wgpu::Features::empty(),
|
// Currently disabled because not all backend support passthrough.
|
||||||
// Currently disabled because not all backend support passthrough.
|
// TODO: reenable only when vulkan adapter is available
|
||||||
// TODO: reenable only when vulkan adapter is available
|
// #[cfg(feature = "passthrough")]
|
||||||
// #[cfg(feature = "passthrough")]
|
// required_features: wgpu::Features::SPIRV_SHADER_PASSTHROUGH,
|
||||||
// required_features: wgpu::Features::SPIRV_SHADER_PASSTHROUGH,
|
required_limits,
|
||||||
required_limits,
|
memory_hints: Default::default(),
|
||||||
memory_hints: Default::default(),
|
trace: wgpu::Trace::Off,
|
||||||
},
|
})
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ mod context;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
pub use context::Context;
|
pub use context::Context;
|
||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
|
use futures::lock::Mutex;
|
||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle};
|
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle, SurfaceId};
|
||||||
use graphene_core::{Color, Ctx};
|
use graphene_core::{Color, Ctx};
|
||||||
pub use graphene_svg_renderer::RenderContext;
|
pub use graphene_svg_renderer::RenderContext;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||||
|
use wgpu::util::TextureBlitter;
|
||||||
use wgpu::{Origin3d, SurfaceConfiguration, TextureAspect};
|
use wgpu::{Origin3d, SurfaceConfiguration, TextureAspect};
|
||||||
|
|
||||||
#[derive(dyn_any::DynAny)]
|
#[derive(dyn_any::DynAny)]
|
||||||
pub struct WgpuExecutor {
|
pub struct WgpuExecutor {
|
||||||
pub context: Context,
|
pub context: Context,
|
||||||
vello_renderer: futures::lock::Mutex<Renderer>,
|
vello_renderer: Mutex<Renderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WgpuExecutor {
|
impl std::fmt::Debug for WgpuExecutor {
|
||||||
@@ -32,16 +34,17 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &
|
|||||||
pub type WgpuSurface = Arc<SurfaceHandle<Surface>>;
|
pub type WgpuSurface = Arc<SurfaceHandle<Surface>>;
|
||||||
pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
|
pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
|
||||||
|
|
||||||
impl graphene_application_io::Size for Surface {
|
|
||||||
fn size(&self) -> UVec2 {
|
|
||||||
self.resolution
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Surface {
|
pub struct Surface {
|
||||||
pub inner: wgpu::Surface<'static>,
|
pub inner: wgpu::Surface<'static>,
|
||||||
resolution: UVec2,
|
pub target_texture: Mutex<Option<TargetTexture>>,
|
||||||
|
pub blitter: TextureBlitter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TargetTexture {
|
||||||
|
view: wgpu::TextureView,
|
||||||
|
size: UVec2,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub type Window = web_sys::HtmlCanvasElement;
|
pub type Window = web_sys::HtmlCanvasElement;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -51,52 +54,88 @@ unsafe impl StaticType for Surface {
|
|||||||
type Static = Surface;
|
type Static = Surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
|
|
||||||
impl WgpuExecutor {
|
impl WgpuExecutor {
|
||||||
pub async fn render_vello_scene(&self, scene: &Scene, surface: &WgpuSurface, width: u32, height: u32, context: &RenderContext, background: Color) -> Result<()> {
|
pub async fn render_vello_scene(&self, scene: &Scene, surface: &WgpuSurface, size: UVec2, context: &RenderContext, background: Color) -> Result<()> {
|
||||||
let surface = &surface.surface.inner;
|
let mut guard = surface.surface.target_texture.lock().await;
|
||||||
let surface_caps = surface.get_capabilities(&self.context.adapter);
|
let target_texture = if let Some(target_texture) = &*guard
|
||||||
surface.configure(
|
&& target_texture.size == size
|
||||||
|
{
|
||||||
|
target_texture
|
||||||
|
} else {
|
||||||
|
let texture = self.context.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: size.x,
|
||||||
|
height: size.y,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
format: VELLO_SURFACE_FORMAT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
*guard = Some(TargetTexture { size, view });
|
||||||
|
guard.as_ref().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let surface_inner = &surface.surface.inner;
|
||||||
|
let surface_caps = surface_inner.get_capabilities(&self.context.adapter);
|
||||||
|
surface_inner.configure(
|
||||||
&self.context.device,
|
&self.context.device,
|
||||||
&SurfaceConfiguration {
|
&SurfaceConfiguration {
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::STORAGE_BINDING,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::STORAGE_BINDING,
|
||||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
format: VELLO_SURFACE_FORMAT,
|
||||||
width,
|
width: size.x,
|
||||||
height,
|
height: size.y,
|
||||||
present_mode: surface_caps.present_modes[0],
|
present_mode: surface_caps.present_modes[0],
|
||||||
alpha_mode: wgpu::CompositeAlphaMode::Opaque,
|
alpha_mode: wgpu::CompositeAlphaMode::Opaque,
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 2,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let surface_texture = surface.get_current_texture()?;
|
|
||||||
|
|
||||||
let [r, g, b, _] = background.to_rgba8_srgb();
|
let [r, g, b, _] = background.to_rgba8_srgb();
|
||||||
let render_params = RenderParams {
|
let render_params = RenderParams {
|
||||||
// We are using an explicit opaque color here to eliminate the alpha premultiplication step
|
// We are using an explicit opaque color here to eliminate the alpha premultiplication step
|
||||||
// which would be required to support a transparent webgpu canvas
|
// which would be required to support a transparent webgpu canvas
|
||||||
base_color: vello::peniko::Color::from_rgba8(r, g, b, 0xff),
|
base_color: vello::peniko::Color::from_rgba8(r, g, b, 0xff),
|
||||||
width,
|
width: size.x,
|
||||||
height,
|
height: size.y,
|
||||||
antialiasing_method: AaConfig::Msaa16,
|
antialiasing_method: AaConfig::Msaa16,
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut renderer = self.vello_renderer.lock().await;
|
let mut renderer = self.vello_renderer.lock().await;
|
||||||
for (id, texture) in context.resource_overrides.iter() {
|
for (id, texture) in context.resource_overrides.iter() {
|
||||||
let texture_view = wgpu::ImageCopyTextureBase {
|
let texture = texture.clone();
|
||||||
texture: texture.clone(),
|
let texture_view = wgpu::TexelCopyTextureInfoBase {
|
||||||
|
texture,
|
||||||
mip_level: 0,
|
mip_level: 0,
|
||||||
origin: Origin3d::ZERO,
|
origin: Origin3d::ZERO,
|
||||||
aspect: TextureAspect::All,
|
aspect: TextureAspect::All,
|
||||||
};
|
};
|
||||||
renderer.override_image(
|
renderer.override_image(
|
||||||
&vello::peniko::Image::new(vello::peniko::Blob::from_raw_parts(Arc::new(vec![]), *id), vello::peniko::Format::Rgba8, 0, 0),
|
&vello::peniko::Image::new(vello::peniko::Blob::from_raw_parts(Arc::new(vec![]), *id), vello::peniko::ImageFormat::Rgba8, 0, 0),
|
||||||
Some(texture_view),
|
Some(texture_view),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
renderer.render_to_surface(&self.context.device, &self.context.queue, scene, &surface_texture, &render_params).unwrap();
|
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let surface_texture = surface_inner.get_current_texture()?;
|
||||||
|
let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Surface Blit") });
|
||||||
|
surface.surface.blitter.copy(
|
||||||
|
&self.context.device,
|
||||||
|
&mut encoder,
|
||||||
|
&target_texture.view,
|
||||||
|
&surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
|
);
|
||||||
|
self.context.queue.submit([encoder.finish()]);
|
||||||
surface_texture.present();
|
surface_texture.present();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -105,24 +144,23 @@ impl WgpuExecutor {
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result<SurfaceHandle<Surface>> {
|
pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result<SurfaceHandle<Surface>> {
|
||||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.surface))?;
|
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.surface))?;
|
||||||
|
self.create_surface_inner(surface, canvas.window_id)
|
||||||
Ok(SurfaceHandle {
|
|
||||||
window_id: canvas.window_id,
|
|
||||||
surface: Surface {
|
|
||||||
inner: surface,
|
|
||||||
resolution: UVec2::ZERO,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn create_surface(&self, window: SurfaceHandle<Window>) -> Result<SurfaceHandle<Surface>> {
|
pub fn create_surface(&self, window: SurfaceHandle<Window>) -> Result<SurfaceHandle<Surface>> {
|
||||||
let size = window.surface.inner_size();
|
|
||||||
let resolution = UVec2::new(size.width, size.height);
|
|
||||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Window(Box::new(window.surface)))?;
|
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Window(Box::new(window.surface)))?;
|
||||||
|
self.create_surface_inner(surface, window.window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_surface_inner(&self, surface: wgpu::Surface<'static>, window_id: SurfaceId) -> Result<SurfaceHandle<Surface>> {
|
||||||
|
let blitter = TextureBlitter::new(&self.context.device, VELLO_SURFACE_FORMAT);
|
||||||
Ok(SurfaceHandle {
|
Ok(SurfaceHandle {
|
||||||
window_id: window.window_id,
|
window_id,
|
||||||
surface: Surface { inner: surface, resolution },
|
surface: Surface {
|
||||||
|
inner: surface,
|
||||||
|
target_texture: Mutex::new(None),
|
||||||
|
blitter,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +172,8 @@ impl WgpuExecutor {
|
|||||||
let vello_renderer = Renderer::new(
|
let vello_renderer = Renderer::new(
|
||||||
&context.device,
|
&context.device,
|
||||||
RendererOptions {
|
RendererOptions {
|
||||||
surface_format: Some(wgpu::TextureFormat::Rgba8Unorm),
|
// surface_format: Some(wgpu::TextureFormat::Rgba8Unorm),
|
||||||
|
pipeline_cache: None,
|
||||||
use_cpu: false,
|
use_cpu: false,
|
||||||
antialiasing_support: AaSupport::all(),
|
antialiasing_support: AaSupport::all(),
|
||||||
num_init_threads: std::num::NonZeroUsize::new(1),
|
num_init_threads: std::num::NonZeroUsize::new(1),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "graphite-proc-macros"
|
name = "graphite-proc-macros"
|
||||||
publish = false
|
publish = false
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.88"
|
||||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
|
|||||||