Merge branch 'master' into grid_shape

This commit is contained in:
0SlowPoke0
2025-07-24 03:31:35 +05:30
committed by GitHub
96 changed files with 4672 additions and 4234 deletions
+3 -3
View File
@@ -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": {
+35 -21
View File
@@ -36,34 +36,50 @@
pkgs-unstable = import nixpkgs-unstable { pkgs-unstable = import nixpkgs-unstable {
inherit system overlays; inherit system overlays;
}; };
rustc-wasm = pkgs.rust-bin.stable.latest.default.override { rustc-wasm = pkgs.rust-bin.stable.latest.default.override {
targets = [ "wasm32-unknown-unknown" ]; targets = [ "wasm32-unknown-unknown" ];
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
@@ -88,12 +104,11 @@
cargo-watch cargo-watch
cargo-nextest cargo-nextest
cargo-expand cargo-expand
# Profiling tools # Profiling tools
gnuplot gnuplot
samply samply
cargo-flamegraph cargo-flamegraph
]; ];
in in
{ {
@@ -101,10 +116,9 @@
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'
Generated
+659 -2724
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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"] }
+1
View File
@@ -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
+1 -4
View File
@@ -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/
+32
View File
@@ -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"
+10
View File
@@ -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");
}
+151
View File
@@ -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();
}
_ => {}
}
}
}
+29
View File
@@ -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 }
}
}
+146
View File
@@ -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,
}
+277
View File
@@ -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
}
}
+386
View File
@@ -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'),
)
}
}
+10
View File
@@ -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;
+61
View File
@@ -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;
}
}
+52
View File
@@ -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;
}
}
+223
View File
@@ -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)
}
}
}
+175
View File
@@ -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();
}
+336
View File
@@ -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);
}
+1 -1
View File
@@ -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(&current_node_id, &[]) { if self.is_layer(&current_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(&current_layer_node) { if !self.document_metadata.structure.contains_key(&current_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(&current_node_id, &[]) { if self.is_layer(&current_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(&current_layer_node) { if !self.document_metadata.structure.contains_key(&current_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(&current_node_id, &[]) { if self.is_layer(&current_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(&current_layer_node) { if !self.document_metadata.structure.contains_key(&current_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
} }
-2
View File
@@ -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",
-45
View File
@@ -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 = [] }
-9
View File
@@ -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"
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

-84
View File
@@ -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
}
-59
View File
@@ -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
}
}
}
+68
View File
@@ -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 -2
View File
@@ -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"]
+11
View File
@@ -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) {
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+14
View File
@@ -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)
/// ///
+30
View File
@@ -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]
} }
+9 -1
View File
@@ -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
+22 -16
View File
@@ -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.
+40 -2
View File
@@ -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
}
+1 -1
View File
@@ -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()
} }
File diff suppressed because it is too large Load Diff
+60 -67
View File
@@ -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")]
+2 -2
View File
@@ -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,
+24 -25
View File
@@ -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.
+18 -19
View File
@@ -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
+193 -209
View File
@@ -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)]
-11
View File
@@ -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
View File
@@ -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;
+78 -6
View File
@@ -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(""))]
+25 -19
View File
@@ -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);
} }
+1 -2
View File
@@ -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 }
+14 -16
View File
@@ -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();
+75 -36
View File
@@ -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),
+1 -1
View File
@@ -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"