mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 17:58:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
@@ -48,11 +48,9 @@ jobs:
|
||||
rustc --version
|
||||
|
||||
- name: ✂ Replace template in <head> of index.html
|
||||
if: github.ref != 'refs/heads/master'
|
||||
env:
|
||||
INDEX_HTML_HEAD_REPLACEMENT: ""
|
||||
run: |
|
||||
# Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
|
||||
git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_REPLACEMENT=""
|
||||
sed -i "s|<!-- INDEX_HTML_HEAD_REPLACEMENT -->|$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
|
||||
|
||||
- name: 🌐 Build Graphite web code
|
||||
|
||||
+2
-6
@@ -1,14 +1,12 @@
|
||||
# This is a helper file for people using NixOS as their operating system.
|
||||
# If you don't know what this file does, you can safely ignore it.
|
||||
# This file defines both the development environment for the project.
|
||||
# This file defines the reproducible development environment for the project.
|
||||
#
|
||||
# Development Environment:
|
||||
# - Provides all necessary tools for Rust/WASM development
|
||||
# - Includes Tauri dependencies for desktop app development
|
||||
# - Provides all necessary tools for Rust/Wasm development
|
||||
# - Sets up profiling and debugging tools
|
||||
# - Configures mold as the default linker for faster builds
|
||||
#
|
||||
#
|
||||
# Usage:
|
||||
# - Development shell: `nix develop`
|
||||
# - Run in dev shell with direnv: add `use flake` to .envrc
|
||||
@@ -16,7 +14,6 @@
|
||||
description = "Development environment and build configuration";
|
||||
|
||||
inputs = {
|
||||
# This url should be changed to match your system packages if you work on tauri because you need to use the same graphics library versions as the ones used by your system
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
@@ -96,7 +93,6 @@
|
||||
pkgs.pkg-config
|
||||
pkgs.git
|
||||
pkgs.gobject-introspection
|
||||
pkgs-unstable.cargo-tauri
|
||||
pkgs-unstable.cargo-about
|
||||
|
||||
# Linker
|
||||
|
||||
Generated
+8
@@ -1834,14 +1834,20 @@ dependencies = [
|
||||
name = "graphite-desktop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cef",
|
||||
"dirs",
|
||||
"futures",
|
||||
"glam",
|
||||
"graph-craft",
|
||||
"graphite-editor",
|
||||
"include_dir",
|
||||
"ron",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wgpu",
|
||||
"wgpu-executor",
|
||||
"winit",
|
||||
]
|
||||
|
||||
@@ -1875,6 +1881,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"usvg",
|
||||
"vello",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -1902,6 +1909,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"math-parser",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+15
-10
@@ -9,17 +9,19 @@ edition = "2024"
|
||||
rust-version = "1.87"
|
||||
|
||||
[features]
|
||||
# default = ["gpu"]
|
||||
# gpu = ["graphite-editor/gpu"]
|
||||
default = ["gpu"]
|
||||
gpu = ["graphite-editor/gpu"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
# graphite-editor = { path = "../editor", features = [
|
||||
# "gpu",
|
||||
# "ron",
|
||||
# "vello",
|
||||
# "decouple-execution",
|
||||
# ] }
|
||||
# # Local dependencies
|
||||
graphite-editor = { path = "../editor", features = [
|
||||
"gpu",
|
||||
"ron",
|
||||
"vello",
|
||||
] }
|
||||
graph-craft = { workspace = true }
|
||||
wgpu-executor = { workspace = true }
|
||||
|
||||
wgpu = { workspace = true }
|
||||
winit = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
@@ -28,4 +30,7 @@ cef = { workspace = true }
|
||||
include_dir = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
dirs = {workspace = true}
|
||||
dirs = { workspace = true }
|
||||
ron = { workspace = true}
|
||||
bytemuck = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
|
||||
+73
-6
@@ -2,6 +2,9 @@ use crate::CustomEvent;
|
||||
use crate::WindowSize;
|
||||
use crate::render::GraphicsState;
|
||||
use crate::render::WgpuContext;
|
||||
use graph_craft::wasm_application_io::WasmApplicationIo;
|
||||
use graphite_editor::application::Editor;
|
||||
use graphite_editor::messages::prelude::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::time::Duration;
|
||||
@@ -21,11 +24,10 @@ pub(crate) struct WinitApp {
|
||||
pub(crate) cef_context: cef::Context<cef::Initialized>,
|
||||
pub(crate) window: Option<Arc<Window>>,
|
||||
cef_schedule: Option<Instant>,
|
||||
_ui_frame_buffer: Option<wgpu::Texture>,
|
||||
window_size_sender: Sender<WindowSize>,
|
||||
_viewport_frame_buffer: Option<wgpu::Texture>,
|
||||
graphics_state: Option<GraphicsState>,
|
||||
wgpu_context: WgpuContext,
|
||||
pub(crate) editor: Editor,
|
||||
}
|
||||
|
||||
impl WinitApp {
|
||||
@@ -34,13 +36,28 @@ impl WinitApp {
|
||||
cef_context,
|
||||
window: None,
|
||||
cef_schedule: Some(Instant::now()),
|
||||
_viewport_frame_buffer: None,
|
||||
_ui_frame_buffer: None,
|
||||
graphics_state: None,
|
||||
window_size_sender,
|
||||
wgpu_context,
|
||||
editor: Editor::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_message(&mut self, message: Message) {
|
||||
let responses = self.editor.handle_message(message);
|
||||
self.send_messages_to_editor(responses);
|
||||
}
|
||||
|
||||
fn send_messages_to_editor(&mut self, responses: Vec<FrontendMessage>) {
|
||||
if responses.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Ok(message) = ron::to_string(&responses) else {
|
||||
tracing::error!("Failed to serialize Messages");
|
||||
return;
|
||||
};
|
||||
self.cef_context.send_web_message(message.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
@@ -49,16 +66,22 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
let timeout = Instant::now() + Duration::from_millis(10);
|
||||
let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout));
|
||||
self.cef_context.work();
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until));
|
||||
}
|
||||
|
||||
fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) {
|
||||
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();
|
||||
}
|
||||
if let StartCause::ResumeTimeReached { .. } = cause {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
@@ -77,13 +100,17 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
self.graphics_state = Some(graphics_state);
|
||||
|
||||
tracing::info!("Winit window created and ready");
|
||||
|
||||
let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone());
|
||||
|
||||
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io));
|
||||
}
|
||||
|
||||
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
|
||||
match event {
|
||||
CustomEvent::UiUpdate(texture) => {
|
||||
if let Some(graphics_state) = self.graphics_state.as_mut() {
|
||||
graphics_state.bind_texture(&texture);
|
||||
graphics_state.bind_ui_texture(&texture);
|
||||
graphics_state.resize(texture.width(), texture.height());
|
||||
}
|
||||
if let Some(window) = &self.window {
|
||||
@@ -97,6 +124,46 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
|
||||
self.cef_schedule = Some(instant);
|
||||
}
|
||||
}
|
||||
CustomEvent::MessageReceived { message } => {
|
||||
if let Message::InputPreprocessor(ipp_message) = &message {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message {
|
||||
if let Some(graphic_state) = &mut self.graphics_state {
|
||||
let window_size = self.window.as_ref().unwrap().inner_size();
|
||||
let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32);
|
||||
let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size;
|
||||
let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size;
|
||||
let offset = top_left.to_array();
|
||||
let scale = (bottom_right - top_left).recip();
|
||||
graphic_state.set_viewport_offset(offset);
|
||||
graphic_state.set_viewport_scale(scale.to_array());
|
||||
} else {
|
||||
panic!("graphics state not intialized, viewport offset might be lost");
|
||||
}
|
||||
}
|
||||
self.dispatch_message(message);
|
||||
}
|
||||
CustomEvent::NodeGraphRan { texture } => {
|
||||
if let Some(texture) = texture
|
||||
&& let Some(graphics_state) = &mut self.graphics_state
|
||||
{
|
||||
graphics_state.bind_viewport_texture(&texture);
|
||||
}
|
||||
let mut responses = VecDeque::new();
|
||||
let err = self.editor.poll_node_graph_evaluation(&mut responses);
|
||||
if let Err(e) = err {
|
||||
if e != "No active document" {
|
||||
tracing::error!("Error poling node graph: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
for message in responses {
|
||||
self.dispatch_message(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ mod context;
|
||||
mod dirs;
|
||||
mod input;
|
||||
mod internal;
|
||||
mod ipc;
|
||||
mod scheme_handler;
|
||||
mod utility;
|
||||
|
||||
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
@@ -19,6 +21,7 @@ pub(crate) trait CefEventHandler: Clone {
|
||||
/// 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);
|
||||
fn receive_web_message(&self, message: &[u8]);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -116,4 +119,16 @@ impl CefEventHandler for CefHandler {
|
||||
fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time));
|
||||
}
|
||||
|
||||
fn receive_web_message(&self, message: &[u8]) {
|
||||
let str = std::str::from_utf8(message).unwrap();
|
||||
match ron::from_str(str) {
|
||||
Ok(message) => {
|
||||
let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived { message });
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize message {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ use winit::event::WindowEvent;
|
||||
use crate::cef::dirs::{cef_cache_dir, cef_data_dir};
|
||||
|
||||
use super::input::InputState;
|
||||
use super::ipc::{MessageType, SendMessage};
|
||||
use super::scheme_handler::{FRONTEND_DOMAIN, GRAPHITE_SCHEME};
|
||||
use super::{CefEventHandler, input};
|
||||
|
||||
use super::internal::{AppImpl, ClientImpl, NonBrowserAppImpl, RenderHandlerImpl};
|
||||
use super::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl};
|
||||
|
||||
pub(crate) struct Setup {}
|
||||
pub(crate) struct Initialized {}
|
||||
@@ -42,7 +43,7 @@ impl Context<Setup> {
|
||||
|
||||
if !is_browser_process {
|
||||
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
|
||||
let mut app = NonBrowserAppImpl::app();
|
||||
let mut app = RenderProcessAppImpl::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()));
|
||||
@@ -70,7 +71,7 @@ impl Context<Setup> {
|
||||
};
|
||||
|
||||
// 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 mut cef_app = App::new(BrowserProcessAppImpl::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 {
|
||||
@@ -81,8 +82,8 @@ impl Context<Setup> {
|
||||
return Err(InitError::InitializationFailed(cef_exit_code));
|
||||
}
|
||||
|
||||
let render_handler = RenderHandlerImpl::new(event_handler.clone());
|
||||
let mut client = Client::new(ClientImpl::new(RenderHandler::new(render_handler)));
|
||||
let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone()));
|
||||
let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone()));
|
||||
|
||||
let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str());
|
||||
|
||||
@@ -129,6 +130,10 @@ impl Context<Initialized> {
|
||||
browser.host().unwrap().was_resized();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_web_message(&self, message: &[u8]) {
|
||||
self.send_message(MessageType::SendToJS, message);
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ContextState> Drop for Context<S> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
mod app;
|
||||
mod browser_process_app;
|
||||
mod browser_process_client;
|
||||
mod browser_process_handler;
|
||||
mod client;
|
||||
mod non_browser_app;
|
||||
mod render_handler;
|
||||
mod render_process_app;
|
||||
mod render_process_handler;
|
||||
mod render_process_v8_handler;
|
||||
|
||||
pub(crate) use app::AppImpl;
|
||||
pub(crate) use client::ClientImpl;
|
||||
pub(crate) use non_browser_app::NonBrowserAppImpl;
|
||||
pub(crate) use browser_process_app::BrowserProcessAppImpl;
|
||||
pub(crate) use browser_process_client::BrowserProcessClientImpl;
|
||||
pub(crate) use render_handler::RenderHandlerImpl;
|
||||
pub(crate) use render_process_app::RenderProcessAppImpl;
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use std::env;
|
||||
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
|
||||
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||
|
||||
use super::browser_process_handler::BrowserProcessHandlerImpl;
|
||||
|
||||
pub(crate) struct AppImpl<H: CefEventHandler> {
|
||||
pub(crate) struct BrowserProcessAppImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler + Clone> AppImpl<H> {
|
||||
impl<H: CefEventHandler + Clone> BrowserProcessAppImpl<H> {
|
||||
pub(crate) fn new(event_handler: H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
@@ -20,7 +23,7 @@ impl<H: CefEventHandler + Clone> AppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> ImplApp for AppImpl<H> {
|
||||
impl<H: CefEventHandler + Clone> ImplApp for BrowserProcessAppImpl<H> {
|
||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.clone())))
|
||||
}
|
||||
@@ -34,6 +37,20 @@ impl<H: CefEventHandler + Clone> ImplApp for AppImpl<H> {
|
||||
// Disable GPU acceleration, because it is not supported for Offscreen Rendering and can cause crashes.
|
||||
cmd.append_switch(Some(&CefString::from("disable-gpu")));
|
||||
cmd.append_switch(Some(&CefString::from("disable-gpu-compositing")));
|
||||
|
||||
// Tell CEF to use Wayland if available
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
let use_wayland = env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|var| !var.is_empty())
|
||||
.or_else(|| env::var("WAYLAND_SOCKET").ok())
|
||||
.filter(|var| !var.is_empty())
|
||||
.is_some();
|
||||
if use_wayland {
|
||||
cmd.append_switch_with_value(Some(&CefString::from("ozone-platform")), Some(&CefString::from("wayland")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +59,7 @@ impl<H: CefEventHandler + Clone> ImplApp for AppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler + Clone> Clone for AppImpl<H> {
|
||||
impl<H: CefEventHandler + Clone> Clone for BrowserProcessAppImpl<H> {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -54,7 +71,7 @@ impl<H: CefEventHandler + Clone> Clone for AppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for AppImpl<H> {
|
||||
impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -62,7 +79,7 @@ impl<H: CefEventHandler> Rc for AppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler + Clone> WrapApp for AppImpl<H> {
|
||||
impl<H: CefEventHandler + Clone> WrapApp for BrowserProcessAppImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
|
||||
use cef::{ImplClient, RenderHandler, WrapClient};
|
||||
|
||||
use crate::cef::CefEventHandler;
|
||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
pub(crate) struct BrowserProcessClientImpl<H: CefEventHandler> {
|
||||
object: *mut RcImpl<_cef_client_t, Self>,
|
||||
render_handler: RenderHandler,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler> BrowserProcessClientImpl<H> {
|
||||
pub(crate) fn new(render_handler: RenderHandler, event_handler: H) -> Self {
|
||||
Self {
|
||||
object: std::ptr::null_mut(),
|
||||
render_handler,
|
||||
event_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
_frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToNative,
|
||||
data,
|
||||
}) => self.event_handler.receive_web_message(data),
|
||||
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in browser process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn render_handler(&self) -> Option<RenderHandler> {
|
||||
Some(self.render_handler.clone())
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_client_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
||||
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(),
|
||||
event_handler: self.event_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for BrowserProcessClientImpl<H> {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapClient for BrowserProcessClientImpl<H> {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+21
-9
@@ -1,38 +1,50 @@
|
||||
use cef::rc::{Rc, RcImpl};
|
||||
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
|
||||
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
|
||||
use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp};
|
||||
|
||||
use super::render_process_handler::RenderProcessHandlerImpl;
|
||||
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
|
||||
|
||||
pub(crate) struct NonBrowserAppImpl {
|
||||
pub(crate) struct RenderProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
render_process_handler: RenderProcessHandler,
|
||||
}
|
||||
impl NonBrowserAppImpl {
|
||||
impl RenderProcessAppImpl {
|
||||
pub(crate) fn app() -> App {
|
||||
App::new(Self { object: std::ptr::null_mut() })
|
||||
App::new(Self {
|
||||
object: std::ptr::null_mut(),
|
||||
render_process_handler: RenderProcessHandler::new(RenderProcessHandlerImpl::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplApp for NonBrowserAppImpl {
|
||||
impl ImplApp for RenderProcessAppImpl {
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
GraphiteSchemeHandlerFactory::register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
|
||||
Some(self.render_process_handler.clone())
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_app_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for NonBrowserAppImpl {
|
||||
impl Clone for RenderProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
Self {
|
||||
object: self.object,
|
||||
render_process_handler: self.render_process_handler.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Rc for NonBrowserAppImpl {
|
||||
impl Rc for RenderProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -40,7 +52,7 @@ impl Rc for NonBrowserAppImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapApp for NonBrowserAppImpl {
|
||||
impl WrapApp for RenderProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use cef::rc::{ConvertReturnValue, Rc, RcImpl};
|
||||
use cef::sys::{_cef_render_process_handler_t, cef_base_ref_counted_t, cef_render_process_handler_t, cef_v8_propertyattribute_t, cef_v8_value_create_array_buffer_with_copy};
|
||||
use cef::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function};
|
||||
|
||||
use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage};
|
||||
|
||||
use super::render_process_v8_handler::BrowserProcessV8HandlerImpl;
|
||||
|
||||
pub(crate) struct RenderProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_render_process_handler_t, Self>,
|
||||
}
|
||||
impl RenderProcessHandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
frame: Option<&mut cef::Frame>,
|
||||
_source_process: cef::ProcessId,
|
||||
message: Option<&mut cef::ProcessMessage>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
|
||||
match unpacked_message {
|
||||
Some(UnpackedMessage {
|
||||
message_type: MessageType::SendToJS,
|
||||
data,
|
||||
}) => {
|
||||
let Some(frame) = frame else {
|
||||
tracing::error!("Frame is not available");
|
||||
return 0;
|
||||
};
|
||||
let Some(context) = frame.v8_context() else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return 0;
|
||||
};
|
||||
if context.enter() == 0 {
|
||||
tracing::error!("Failed to enter V8 context");
|
||||
return 0;
|
||||
}
|
||||
let mut value: V8Value = unsafe { cef_v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut std::ffi::c_void, data.len()) }.wrap_result();
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return 0;
|
||||
};
|
||||
|
||||
let function_name = "receiveNativeMessage";
|
||||
let property_name = "receiveNativeMessageData";
|
||||
|
||||
let function_call = format!("window.{function_name}(window.{property_name})");
|
||||
|
||||
global.set_value_bykey(
|
||||
Some(&CefString::from(property_name)),
|
||||
Some(&mut value),
|
||||
cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result(),
|
||||
);
|
||||
|
||||
if global.value_bykey(Some(&CefString::from(function_name))).is_some() {
|
||||
frame.execute_java_script(Some(&CefString::from(function_call.as_str())), None, 0);
|
||||
}
|
||||
|
||||
if context.exit() == 0 {
|
||||
tracing::error!("Failed to exit V8 context");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("Unexpected message type received in render process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
|
||||
let function_name = "sendNativeMessage";
|
||||
|
||||
let Some(context) = context else {
|
||||
tracing::error!("V8 context is not available");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut v8_handler = V8Handler::new(BrowserProcessV8HandlerImpl::new());
|
||||
let Some(mut function) = v8_value_create_function(Some(&CefString::from(function_name)), Some(&mut v8_handler)) else {
|
||||
tracing::error!("Failed to create V8 function {function_name}");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(global) = context.global() else {
|
||||
tracing::error!("Global object is not available in V8 context");
|
||||
return;
|
||||
};
|
||||
global.set_value_bykey(Some(&CefString::from(function_name)), Some(&mut function), V8Propertyattribute::default());
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RenderProcessHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
impl Rc for RenderProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WrapRenderProcessHandler for RenderProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context};
|
||||
|
||||
use crate::cef::ipc::{MessageType, SendMessage};
|
||||
|
||||
pub struct BrowserProcessV8HandlerImpl {
|
||||
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
|
||||
}
|
||||
|
||||
impl BrowserProcessV8HandlerImpl {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { object: std::ptr::null_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ImplV8Handler for BrowserProcessV8HandlerImpl {
|
||||
fn execute(
|
||||
&self,
|
||||
name: Option<&cef::CefString>,
|
||||
_object: Option<&mut V8Value>,
|
||||
arguments: Option<&[Option<V8Value>]>,
|
||||
_retval: Option<&mut Option<V8Value>>,
|
||||
_exception: Option<&mut cef::CefString>,
|
||||
) -> ::std::os::raw::c_int {
|
||||
if let Some(name) = name {
|
||||
if name.to_string() == "sendNativeMessage" {
|
||||
let Some(args) = arguments else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = args.first() else {
|
||||
tracing::error!("No arguments provided to sendNativeMessage");
|
||||
return 0;
|
||||
};
|
||||
let Some(arg1) = arg1.as_ref() else {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
};
|
||||
if arg1.is_array_buffer() == 0 {
|
||||
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let size = arg1.array_buffer_byte_length();
|
||||
let ptr = arg1.array_buffer_data();
|
||||
let data = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, size) };
|
||||
|
||||
v8_context_get_current_context().send_message(MessageType::SendToNative, data);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut cef::sys::_cef_v8_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for BrowserProcessV8HandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
rc_impl.interface.add_ref();
|
||||
}
|
||||
Self { object: self.object }
|
||||
}
|
||||
}
|
||||
|
||||
impl Rc for BrowserProcessV8HandlerImpl {
|
||||
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
std::mem::transmute(&base.cef_object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapV8Handler for BrowserProcessV8HandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
|
||||
|
||||
use super::{Context, Initialized};
|
||||
|
||||
pub(crate) enum MessageType {
|
||||
SendToJS,
|
||||
SendToNative,
|
||||
}
|
||||
impl From<MessageType> for MessageInfo {
|
||||
fn from(val: MessageType) -> Self {
|
||||
match val {
|
||||
MessageType::SendToJS => MessageInfo {
|
||||
name: "send_to_js".to_string(),
|
||||
target: cef_process_id_t::PID_RENDERER.into(),
|
||||
},
|
||||
MessageType::SendToNative => MessageInfo {
|
||||
name: "send_to_native".to_string(),
|
||||
target: cef_process_id_t::PID_BROWSER.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<String> for MessageType {
|
||||
type Error = ();
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"send_to_js" => Ok(MessageType::SendToJS),
|
||||
"send_to_native" => Ok(MessageType::SendToNative),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MessageInfo {
|
||||
name: String,
|
||||
target: ProcessId,
|
||||
}
|
||||
|
||||
pub(crate) trait SendMessage {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]);
|
||||
}
|
||||
impl SendMessage for Context<Initialized> {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(browser) = &self.browser else {
|
||||
tracing::error!("Browser is not initialized, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(frame) = browser.main_frame() else {
|
||||
tracing::error!("Main frame is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for Option<V8Context> {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(context) = self else {
|
||||
tracing::error!("Current V8 context is not available, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
context.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for V8Context {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let Some(frame) = self.frame() else {
|
||||
tracing::error!("Current V8 context does not have a frame, cannot send message");
|
||||
return;
|
||||
};
|
||||
|
||||
frame.send_message(message_type, message);
|
||||
}
|
||||
}
|
||||
impl SendMessage for Frame {
|
||||
fn send_message(&self, message_type: MessageType, message: &[u8]) {
|
||||
let MessageInfo { name, target } = message_type.into();
|
||||
|
||||
let Some(mut process_message) = cef::process_message_create(Some(&CefString::from(name.as_str()))) else {
|
||||
tracing::error!("Failed to create process message: {}", name);
|
||||
return;
|
||||
};
|
||||
let Some(arg_list) = process_message.argument_list() else { return };
|
||||
let mut value = ::cef::binary_value_create(Some(message));
|
||||
arg_list.set_binary(0, value.as_mut());
|
||||
|
||||
self.send_process_message(target, Some(&mut process_message));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UnpackedMessage<'a> {
|
||||
pub(crate) message_type: MessageType,
|
||||
pub(crate) data: &'a [u8],
|
||||
}
|
||||
|
||||
trait Sealed {}
|
||||
impl Sealed for cef::ProcessMessage {}
|
||||
#[allow(private_bounds)]
|
||||
pub(crate) trait UnpackMessage: Sealed {
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the message is valid.
|
||||
/// Message should come from cef.
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>>;
|
||||
}
|
||||
impl UnpackMessage for cef::ProcessMessage {
|
||||
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>> {
|
||||
let pointer: *mut cef::sys::_cef_string_utf16_t = self.name().into();
|
||||
let message = unsafe { super::utility::pointer_to_string(pointer) };
|
||||
let Ok(message_type) = message.try_into() else {
|
||||
tracing::error!("Failed to get message type from process message");
|
||||
return None;
|
||||
};
|
||||
let arglist = self.argument_list()?;
|
||||
let binary = arglist.binary(0)?;
|
||||
let size = binary.size();
|
||||
let ptr = binary.raw_data();
|
||||
let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) };
|
||||
Some(UnpackedMessage { message_type, data: buffer })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub unsafe fn pointer_to_string(pointer: *mut cef::sys::_cef_string_utf16_t) -> String {
|
||||
let str = unsafe { (*pointer).str_ };
|
||||
let len = unsafe { (*pointer).length };
|
||||
let slice = unsafe { std::slice::from_raw_parts(str, len) };
|
||||
String::from_utf16(slice).unwrap()
|
||||
}
|
||||
+22
-2
@@ -1,7 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
use std::process::exit;
|
||||
use std::time::Instant;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
use graphite_editor::messages::prelude::Message;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
@@ -20,6 +21,8 @@ mod dirs;
|
||||
pub(crate) enum CustomEvent {
|
||||
UiUpdate(wgpu::Texture),
|
||||
ScheduleBrowserWork(Instant),
|
||||
MessageReceived { message: Message },
|
||||
NodeGraphRan { texture: Option<wgpu::Texture> },
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -38,7 +41,7 @@ fn main() {
|
||||
|
||||
let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel();
|
||||
|
||||
let wgpu_context = futures::executor::block_on(WgpuContext::new());
|
||||
let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap();
|
||||
let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) {
|
||||
Ok(c) => c,
|
||||
Err(cef::InitError::AlreadyRunning) => {
|
||||
@@ -53,6 +56,23 @@ fn main() {
|
||||
|
||||
tracing::info!("Cef initialized successfully");
|
||||
|
||||
let rendering_loop_proxy = event_loop.create_proxy();
|
||||
let target_fps = 60;
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
let last_render = Instant::now();
|
||||
let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph());
|
||||
if has_run {
|
||||
let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan {
|
||||
texture: texture.map(|t| (*t.texture).clone()),
|
||||
});
|
||||
}
|
||||
let frame_time = Duration::from_secs_f32((target_fps as f32).recip());
|
||||
let sleep = last_render + frame_time - Instant::now();
|
||||
std::thread::sleep(sleep);
|
||||
}
|
||||
});
|
||||
|
||||
let mut winit_app = WinitApp::new(cef_context, window_size_sender, wgpu_context);
|
||||
|
||||
event_loop.run_app(&mut winit_app).unwrap();
|
||||
|
||||
+69
-49
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use thiserror::Error;
|
||||
use winit::window::Window;
|
||||
|
||||
@@ -55,54 +56,20 @@ pub(crate) enum FrameBufferError {
|
||||
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WgpuContext {
|
||||
pub(crate) device: wgpu::Device,
|
||||
pub(crate) queue: wgpu::Queue,
|
||||
adapter: wgpu::Adapter,
|
||||
instance: wgpu::Instance,
|
||||
}
|
||||
|
||||
impl WgpuContext {
|
||||
pub(crate) async fn new() -> Self {
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: None,
|
||||
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();
|
||||
|
||||
Self { device, queue, adapter, instance }
|
||||
}
|
||||
}
|
||||
pub use wgpu_executor::Context as WgpuContext;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GraphicsState {
|
||||
surface: wgpu::Surface<'static>,
|
||||
context: WgpuContext,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
render_pipeline: wgpu::RenderPipeline,
|
||||
sampler: wgpu::Sampler,
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
viewport_texture: Option<wgpu::Texture>,
|
||||
ui_texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
}
|
||||
|
||||
impl GraphicsState {
|
||||
@@ -156,6 +123,16 @@ impl GraphicsState {
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
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: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
@@ -166,7 +143,10 @@ impl GraphicsState {
|
||||
let render_pipeline_layout = context.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Render Pipeline Layout"),
|
||||
bind_group_layouts: &[&texture_bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
push_constant_ranges: &[wgpu::PushConstantRange {
|
||||
stages: wgpu::ShaderStages::FRAGMENT,
|
||||
range: 0..size_of::<Constants>() as u32,
|
||||
}],
|
||||
});
|
||||
|
||||
let render_pipeline = context.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
@@ -211,10 +191,13 @@ impl GraphicsState {
|
||||
surface,
|
||||
context,
|
||||
config,
|
||||
texture: None,
|
||||
bind_group: None,
|
||||
render_pipeline,
|
||||
sampler,
|
||||
viewport_scale: [1.0, 1.0],
|
||||
viewport_offset: [0.0, 0.0],
|
||||
viewport_texture: None,
|
||||
ui_texture: None,
|
||||
bind_group: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,25 +209,47 @@ impl GraphicsState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bind_texture(&mut self, texture: &wgpu::Texture) {
|
||||
let bind_group = self.create_bindgroup(texture);
|
||||
self.texture = Some(texture.clone());
|
||||
pub(crate) fn bind_ui_texture(&mut self, texture: &wgpu::Texture) {
|
||||
let bind_group = self.create_bindgroup(texture, &self.viewport_texture.clone().unwrap_or(texture.clone()));
|
||||
|
||||
self.ui_texture = Some(texture.clone());
|
||||
|
||||
self.bind_group = Some(bind_group);
|
||||
}
|
||||
|
||||
fn create_bindgroup(&self, texture: &wgpu::Texture) -> wgpu::BindGroup {
|
||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
pub(crate) fn bind_viewport_texture(&mut self, texture: &wgpu::Texture) {
|
||||
let bind_group = self.create_bindgroup(&self.ui_texture.clone().unwrap_or(texture.clone()), texture);
|
||||
|
||||
self.viewport_texture = Some(texture.clone());
|
||||
|
||||
self.bind_group = Some(bind_group);
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_scale(&mut self, scale: [f32; 2]) {
|
||||
self.viewport_scale = scale;
|
||||
}
|
||||
|
||||
pub(crate) fn set_viewport_offset(&mut self, offset: [f32; 2]) {
|
||||
self.viewport_offset = offset;
|
||||
}
|
||||
|
||||
fn create_bindgroup(&self, ui_texture: &wgpu::Texture, viewport_texture: &wgpu::Texture) -> wgpu::BindGroup {
|
||||
let ui_texture_view = ui_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let viewport_texture_view = viewport_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
self.context.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),
|
||||
resource: wgpu::BindingResource::TextureView(&ui_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&viewport_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
],
|
||||
@@ -275,6 +280,14 @@ impl GraphicsState {
|
||||
});
|
||||
|
||||
render_pass.set_pipeline(&self.render_pipeline);
|
||||
render_pass.set_push_constants(
|
||||
wgpu::ShaderStages::FRAGMENT,
|
||||
0,
|
||||
bytemuck::bytes_of(&Constants {
|
||||
viewport_scale: self.viewport_scale,
|
||||
viewport_offset: self.viewport_offset,
|
||||
}),
|
||||
);
|
||||
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
|
||||
@@ -288,3 +301,10 @@ impl GraphicsState {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
struct Constants {
|
||||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
}
|
||||
|
||||
@@ -25,12 +25,27 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
return out;
|
||||
}
|
||||
|
||||
struct Constants {
|
||||
viewport_scale: vec2<f32>,
|
||||
viewport_offset: vec2<f32>,
|
||||
};
|
||||
|
||||
var<push_constant> constants: Constants;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_diffuse: texture_2d<f32>;
|
||||
var t_ui: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var t_viewport: texture_2d<f32>;
|
||||
@group(0) @binding(2)
|
||||
var s_diffuse: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
|
||||
let ui_color: vec4<f32> = textureSample(t_ui, s_diffuse, in.tex_coords);
|
||||
if (ui_color.a == 1.0) {
|
||||
return ui_color;
|
||||
}
|
||||
let viewport_tex_coords = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
|
||||
let viewport_color: vec4<f32> = textureSample(t_viewport, s_diffuse, viewport_tex_coords);
|
||||
return ui_color * ui_color.a + viewport_color * (1.0 - ui_color.a);
|
||||
}
|
||||
|
||||
+1
-2
@@ -14,8 +14,6 @@ license = "Apache-2.0"
|
||||
default = ["wasm"]
|
||||
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
||||
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
|
||||
tauri = ["ron", "decouple-execution"]
|
||||
decouple-execution = []
|
||||
resvg = ["graphene-std/resvg"]
|
||||
vello = ["graphene-std/vello", "resvg"]
|
||||
ron = ["dep:ron"]
|
||||
@@ -47,6 +45,7 @@ usvg = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
spin = "0.9.8"
|
||||
|
||||
+17
-43
@@ -5,7 +5,6 @@ use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dispatcher {
|
||||
buffered_queue: Option<Vec<VecDeque<Message>>>,
|
||||
message_queues: Vec<VecDeque<Message>>,
|
||||
pub responses: Vec<FrontendMessage>,
|
||||
pub message_handlers: DispatcherMessageHandlers,
|
||||
@@ -14,8 +13,10 @@ pub struct Dispatcher {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DispatcherMessageHandlers {
|
||||
animation_message_handler: AnimationMessageHandler,
|
||||
app_window_message_handler: AppWindowMessageHandler,
|
||||
broadcast_message_handler: BroadcastMessageHandler,
|
||||
debug_message_handler: DebugMessageHandler,
|
||||
defer_message_handler: DeferMessageHandler,
|
||||
dialog_message_handler: DialogMessageHandler,
|
||||
globals_message_handler: GlobalsMessageHandler,
|
||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||
@@ -50,7 +51,10 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame))];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
||||
];
|
||||
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
|
||||
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
|
||||
|
||||
@@ -90,14 +94,6 @@ impl Dispatcher {
|
||||
|
||||
pub fn handle_message<T: Into<Message>>(&mut self, message: T, process_after_all_current: bool) {
|
||||
let message = message.into();
|
||||
// Add all additional messages to the buffer if it exists (except from the end buffer message)
|
||||
if !matches!(message, Message::EndBuffer { .. }) {
|
||||
if let Some(buffered_queue) = &mut self.buffered_queue {
|
||||
Self::schedule_execution(buffered_queue, true, [message]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are not maintaining the buffer, simply add to the current queue
|
||||
Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]);
|
||||
@@ -129,10 +125,16 @@ impl Dispatcher {
|
||||
Message::Animation(message) => {
|
||||
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::AppWindow(message) => {
|
||||
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
|
||||
Message::Debug(message) => {
|
||||
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Defer(message) => {
|
||||
self.message_handlers.defer_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Dialog(message) => {
|
||||
let context = DialogMessageContext {
|
||||
portfolio: &self.message_handlers.portfolio_message_handler,
|
||||
@@ -204,11 +206,14 @@ impl Dispatcher {
|
||||
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Tool(message) => {
|
||||
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
|
||||
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
|
||||
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
|
||||
warn!("Called ToolMessage without an active document.\nGot {message:?}");
|
||||
return;
|
||||
};
|
||||
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
|
||||
warn!("Called ToolMessage with an invalid active document.\nGot {message:?}");
|
||||
return;
|
||||
};
|
||||
|
||||
let context = ToolMessageContext {
|
||||
document_id,
|
||||
@@ -228,37 +233,6 @@ impl Dispatcher {
|
||||
Message::Batched { messages } => {
|
||||
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
|
||||
}
|
||||
Message::StartBuffer => {
|
||||
self.buffered_queue = Some(std::mem::take(&mut self.message_queues));
|
||||
}
|
||||
Message::EndBuffer { render_metadata } => {
|
||||
// Assign the message queue to the currently buffered queue
|
||||
if let Some(buffered_queue) = self.buffered_queue.take() {
|
||||
self.cleanup_queues(false);
|
||||
assert!(self.message_queues.is_empty(), "message queues are always empty when ending a buffer");
|
||||
self.message_queues = buffered_queue;
|
||||
};
|
||||
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
let messages = [
|
||||
DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
},
|
||||
DocumentMessage::UpdateClickTargets { click_targets },
|
||||
DocumentMessage::UpdateClipTargets { clip_targets },
|
||||
];
|
||||
Self::schedule_execution(&mut self.message_queues, false, messages.map(Message::from));
|
||||
}
|
||||
}
|
||||
|
||||
// If there are child messages, append the queue to the list of queues
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, AppWindow)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppWindowMessage {
|
||||
AppWindowMinimize,
|
||||
AppWindowMaximize,
|
||||
AppWindowClose,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::messages::app_window::AppWindowMessage;
|
||||
use crate::messages::prelude::*;
|
||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct AppWindowMessageHandler {
|
||||
platform: AppWindowPlatform,
|
||||
maximized: bool,
|
||||
viewport_hole_punch_active: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
AppWindowMessage::AppWindowMinimize => {
|
||||
self.platform = if self.platform == AppWindowPlatform::Mac {
|
||||
AppWindowPlatform::Windows
|
||||
} else {
|
||||
AppWindowPlatform::Mac
|
||||
};
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
AppWindowMessage::AppWindowMaximize => {
|
||||
self.maximized = !self.maximized;
|
||||
responses.add(FrontendMessage::UpdateMaximized { maximized: self.maximized });
|
||||
|
||||
self.viewport_hole_punch_active = !self.viewport_hole_punch_active;
|
||||
responses.add(FrontendMessage::UpdateViewportHolePunch {
|
||||
active: self.viewport_hole_punch_active,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
self.platform = AppWindowPlatform::Web;
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(AppWindowMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum AppWindowPlatform {
|
||||
#[default]
|
||||
Web,
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod app_window_message;
|
||||
pub mod app_window_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use app_window_message::{AppWindowMessage, AppWindowMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use app_window_message_handler::AppWindowMessageHandler;
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Defer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DeferMessage {
|
||||
SetGraphSubmissionIndex(u64),
|
||||
TriggerGraphRun(u64),
|
||||
AfterGraphRun { messages: Vec<Message> },
|
||||
TriggerNavigationReady,
|
||||
AfterNavigationReady { messages: Vec<Message> },
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Default, ExtractField)]
|
||||
pub struct DeferMessageHandler {
|
||||
after_graph_run: Vec<(u64, Message)>,
|
||||
after_viewport_resize: Vec<Message>,
|
||||
current_graph_submission_id: u64,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<DeferMessage, ()> for DeferMessageHandler {
|
||||
fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
DeferMessage::AfterGraphRun { mut messages } => {
|
||||
self.after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
|
||||
}
|
||||
DeferMessage::AfterNavigationReady { messages } => {
|
||||
self.after_viewport_resize.extend_from_slice(&messages);
|
||||
}
|
||||
DeferMessage::SetGraphSubmissionIndex(execution_id) => {
|
||||
self.current_graph_submission_id = execution_id + 1;
|
||||
}
|
||||
DeferMessage::TriggerGraphRun(execution_id) => {
|
||||
if self.after_graph_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Find the index of the last message we can process
|
||||
let num_elements_to_remove = self.after_graph_run.binary_search_by_key(&(execution_id + 1), |x| x.0).unwrap_or_else(|pos| pos - 1);
|
||||
let elements = self.after_graph_run.drain(0..=num_elements_to_remove);
|
||||
for (_, message) in elements.rev() {
|
||||
responses.add_front(message);
|
||||
}
|
||||
}
|
||||
DeferMessage::TriggerNavigationReady => {
|
||||
for message in self.after_viewport_resize.drain(..).rev() {
|
||||
responses.add_front(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(DeferMessageDiscriminant;
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod defer_message;
|
||||
mod defer_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use defer_message::{DeferMessage, DeferMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use defer_message_handler::DeferMessageHandler;
|
||||
+12
-10
@@ -24,18 +24,20 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
|
||||
|
||||
let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0;
|
||||
if create_artboard {
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
id: NodeId::new(),
|
||||
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![
|
||||
GraphOperationMessage::NewArtboard {
|
||||
id: NodeId::new(),
|
||||
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
});
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()],
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
|
||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
|
||||
@@ -58,7 +59,6 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "commitDate")]
|
||||
commit_date: String,
|
||||
},
|
||||
TriggerDelayedZoomCanvasToFitAll,
|
||||
TriggerDownloadImage {
|
||||
svg: String,
|
||||
name: String,
|
||||
@@ -309,4 +309,13 @@ pub enum FrontendMessage {
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdatePlatform {
|
||||
platform: AppWindowPlatform,
|
||||
},
|
||||
UpdateMaximized {
|
||||
maximized: bool,
|
||||
},
|
||||
UpdateViewportHolePunch {
|
||||
active: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control, Shift])),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control])),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
|
||||
@@ -78,7 +78,9 @@ impl<'a> serde::Deserialize<'a> for CheckboxId {
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
let id = u64::deserialize(deserializer)?;
|
||||
let optional_id: Option<u64> = Option::deserialize(deserializer)?;
|
||||
// TODO: This is potentially weird because after deserialization the two labels will be decoupled if the value not existent
|
||||
let id = optional_id.unwrap_or(0);
|
||||
let checkbox_id = CheckboxId(OnceCell::new().into());
|
||||
checkbox_id.0.set(id).map_err(serde::de::Error::custom)?;
|
||||
Ok(checkbox_id)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::renderer::RenderMetadata;
|
||||
use graphite_proc_macros::*;
|
||||
|
||||
#[impl_message]
|
||||
@@ -9,10 +8,14 @@ pub enum Message {
|
||||
#[child]
|
||||
Animation(AnimationMessage),
|
||||
#[child]
|
||||
AppWindow(AppWindowMessage),
|
||||
#[child]
|
||||
Broadcast(BroadcastMessage),
|
||||
#[child]
|
||||
Debug(DebugMessage),
|
||||
#[child]
|
||||
Defer(DeferMessage),
|
||||
#[child]
|
||||
Dialog(DialogMessage),
|
||||
#[child]
|
||||
Frontend(FrontendMessage),
|
||||
@@ -38,10 +41,6 @@ pub enum Message {
|
||||
Batched {
|
||||
messages: Box<[Message]>,
|
||||
},
|
||||
StartBuffer,
|
||||
EndBuffer {
|
||||
render_metadata: RenderMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! The root-level messages forming the first layer of the message system architecture.
|
||||
|
||||
pub mod animation;
|
||||
pub mod app_window;
|
||||
pub mod broadcast;
|
||||
pub mod debug;
|
||||
pub mod defer;
|
||||
pub mod dialog;
|
||||
pub mod frontend;
|
||||
pub mod globals;
|
||||
|
||||
@@ -1435,6 +1435,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
},
|
||||
})
|
||||
}
|
||||
// Some parts of the editior (e.g. navigation messages) depend on these bounds to be present
|
||||
let bounds = if self.graph_view_overlay_open {
|
||||
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path).cloned()
|
||||
} else {
|
||||
self.network_interface.document_bounds_document_space(true)
|
||||
};
|
||||
if bounds.is_some() {
|
||||
responses.add(DeferMessage::TriggerNavigationReady);
|
||||
} else {
|
||||
// If we don't have bounds yet, we need wait until the node graph has run once more
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::PTZUpdate.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectionStepBack => {
|
||||
self.network_interface.selection_step_back(&self.selection_network_path);
|
||||
@@ -1870,10 +1884,11 @@ impl DocumentMessageHandler {
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
|
||||
|
||||
// TODO: Remove once the footprint is used to load the imports/export distances from the edge
|
||||
responses.add(NodeGraphMessage::UnloadWires);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
Some(previous_network)
|
||||
}
|
||||
pub fn redo_with_history(&mut self, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -630,8 +630,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true),
|
||||
NodeInput::value(
|
||||
TaggedValue::Footprint(Footprint {
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)),
|
||||
resolution: UVec2::new(100, 100),
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
|
||||
resolution: UVec2::new(1000, 1000),
|
||||
..Default::default()
|
||||
}),
|
||||
false,
|
||||
@@ -851,7 +851,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Coordinate",
|
||||
identifier: "Split Vec2",
|
||||
category: "Math: Vector",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
@@ -882,7 +882,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_metadata: vec![("Coordinate", "TODO").into()],
|
||||
input_metadata: vec![("Vec2", "TODO").into()],
|
||||
output_names: vec!["X".to_string(), "Y".to_string()],
|
||||
has_primary_output: false,
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
@@ -917,7 +917,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed(
|
||||
"Decomposes the X and Y components of a 2D coordinate.\n\nThe inverse of this node is \"Coordinate Value\", which can have either or both its X and Y exposed as graph inputs.",
|
||||
"Decomposes the X and Y components of a vec2.\n\nThe inverse of this node is \"Vec2 Value\", which can have either or both its X and Y parameters exposed as graph inputs.",
|
||||
),
|
||||
properties: None,
|
||||
},
|
||||
@@ -2042,7 +2042,7 @@ fn static_input_properties() -> InputProperties {
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(vec![node_properties::coordinate_widget(
|
||||
Ok(vec![node_properties::vec2_widget(
|
||||
ParameterWidgetsInfo::new(node_id, index, true, context),
|
||||
&x,
|
||||
&y,
|
||||
@@ -2298,7 +2298,7 @@ fn static_input_properties() -> InputProperties {
|
||||
"spline_input".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
Ok(vec![LayoutGroup::Row {
|
||||
widgets: node_properties::array_of_coordinates_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
|
||||
widgets: node_properties::array_of_vec2_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
|
||||
}])
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -160,7 +160,7 @@ pub(crate) fn property_from_type(
|
||||
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
|
||||
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
|
||||
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
|
||||
Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
|
||||
Some("TextArea") => text_area_widget(default_info).into(),
|
||||
|
||||
// For all other types, use TypeId-based matching
|
||||
@@ -175,13 +175,13 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
|
||||
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
|
||||
// ==========================
|
||||
// PRIMITIVE COLLECTION TYPES
|
||||
// ==========================
|
||||
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_coordinates_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
|
||||
// ====================
|
||||
// GRAPHICAL DATA TYPES
|
||||
// ====================
|
||||
@@ -480,6 +480,10 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
|
||||
resolution_widgets.push(
|
||||
NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x * 100.))
|
||||
.label("Resolution")
|
||||
.mode_range()
|
||||
.min(0.)
|
||||
.range_min(Some(1.))
|
||||
.range_max(Some(100.))
|
||||
.unit("%")
|
||||
.on_update(update_value(
|
||||
move |x: &NumberInput| {
|
||||
@@ -626,7 +630,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
|
||||
pub fn vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info);
|
||||
@@ -723,7 +727,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
|
||||
widgets
|
||||
}
|
||||
|
||||
pub fn array_of_coordinates_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info);
|
||||
@@ -1249,7 +1253,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
let spacing = coordinate_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
|
||||
let spacing = vec2_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
|
||||
widgets.push(spacing);
|
||||
}
|
||||
GridType::Isometric => {
|
||||
@@ -1259,7 +1263,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
NumberInput::default().label("H").min(0.).unit(" px"),
|
||||
),
|
||||
};
|
||||
let angles = coordinate_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
|
||||
let angles = vec2_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
|
||||
widgets.extend([spacing, angles]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ pub mod grid_overlays;
|
||||
mod overlays_message;
|
||||
mod overlays_message_handler;
|
||||
pub mod utility_functions;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod utility_types;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod utility_types_vello;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use utility_types_vello as utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
|
||||
|
||||
@@ -21,7 +21,6 @@ pub struct OverlaysMessageHandler {
|
||||
impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMessageHandler {
|
||||
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, context: OverlaysMessageContext) {
|
||||
let OverlaysMessageContext { visibility_settings, ipp, .. } = context;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let device_pixel_ratio = context.device_pixel_ratio;
|
||||
|
||||
match message {
|
||||
@@ -69,9 +68,39 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(test)]
|
||||
OverlaysMessage::Draw => {}
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(test)))]
|
||||
OverlaysMessage::Draw => {
|
||||
warn!("Cannot render overlays on non-Wasm targets.\n{responses:?} {visibility_settings:?} {ipp:?}",);
|
||||
use super::utility_types::OverlayContext;
|
||||
use vello::Scene;
|
||||
|
||||
let size = ipp.viewport_bounds.size().as_uvec2();
|
||||
|
||||
let scene = Scene::new();
|
||||
|
||||
if visibility_settings.all() {
|
||||
let overlay_context = OverlayContext {
|
||||
scene,
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings,
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::GridOverlays(overlay_context.clone()));
|
||||
|
||||
for provider in &self.overlay_providers {
|
||||
let overlay_context = OverlayContext {
|
||||
scene: Scene::new(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings,
|
||||
};
|
||||
responses.add(provider(overlay_context));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Render the Vello scene to a texture and display it
|
||||
}
|
||||
OverlaysMessage::AddProvider(message) => {
|
||||
self.overlay_providers.insert(message);
|
||||
|
||||
@@ -423,11 +423,9 @@ impl OverlayContext {
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, dash_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
|
||||
let end_point2 = pivot + dash_radius * DVec2::from_angle(offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
@@ -592,9 +590,9 @@ impl OverlayContext {
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, dash_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, dash_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,758 @@
|
||||
use crate::consts::{
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
|
||||
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorData};
|
||||
use std::collections::HashMap;
|
||||
use vello::Scene;
|
||||
use vello::kurbo::{self, BezPath};
|
||||
use vello::peniko;
|
||||
|
||||
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
||||
|
||||
pub fn empty_provider() -> OverlayProvider {
|
||||
|_| Message::NoOp
|
||||
}
|
||||
|
||||
// Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum OverlaysType {
|
||||
ArtboardName,
|
||||
CompassRose,
|
||||
QuickMeasurement,
|
||||
TransformMeasurement,
|
||||
TransformCage,
|
||||
HoverOutline,
|
||||
SelectionOutline,
|
||||
Pivot,
|
||||
Origin,
|
||||
Path,
|
||||
Anchors,
|
||||
Handles,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct OverlaysVisibilitySettings {
|
||||
pub all: bool,
|
||||
pub artboard_name: bool,
|
||||
pub compass_rose: bool,
|
||||
pub quick_measurement: bool,
|
||||
pub transform_measurement: bool,
|
||||
pub transform_cage: bool,
|
||||
pub hover_outline: bool,
|
||||
pub selection_outline: bool,
|
||||
pub pivot: bool,
|
||||
pub origin: bool,
|
||||
pub path: bool,
|
||||
pub anchors: bool,
|
||||
pub handles: bool,
|
||||
}
|
||||
|
||||
impl Default for OverlaysVisibilitySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
all: true,
|
||||
artboard_name: true,
|
||||
compass_rose: true,
|
||||
quick_measurement: true,
|
||||
transform_measurement: true,
|
||||
transform_cage: true,
|
||||
hover_outline: true,
|
||||
selection_outline: true,
|
||||
pivot: true,
|
||||
origin: true,
|
||||
path: true,
|
||||
anchors: true,
|
||||
handles: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OverlaysVisibilitySettings {
|
||||
pub fn all(&self) -> bool {
|
||||
self.all
|
||||
}
|
||||
|
||||
pub fn artboard_name(&self) -> bool {
|
||||
self.all && self.artboard_name
|
||||
}
|
||||
|
||||
pub fn compass_rose(&self) -> bool {
|
||||
self.all && self.compass_rose
|
||||
}
|
||||
|
||||
pub fn quick_measurement(&self) -> bool {
|
||||
self.all && self.quick_measurement
|
||||
}
|
||||
|
||||
pub fn transform_measurement(&self) -> bool {
|
||||
self.all && self.transform_measurement
|
||||
}
|
||||
|
||||
pub fn transform_cage(&self) -> bool {
|
||||
self.all && self.transform_cage
|
||||
}
|
||||
|
||||
pub fn hover_outline(&self) -> bool {
|
||||
self.all && self.hover_outline
|
||||
}
|
||||
|
||||
pub fn selection_outline(&self) -> bool {
|
||||
self.all && self.selection_outline
|
||||
}
|
||||
|
||||
pub fn pivot(&self) -> bool {
|
||||
self.all && self.pivot
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> bool {
|
||||
self.all && self.origin
|
||||
}
|
||||
|
||||
pub fn path(&self) -> bool {
|
||||
self.all && self.path
|
||||
}
|
||||
|
||||
pub fn anchors(&self) -> bool {
|
||||
self.all && self.anchors
|
||||
}
|
||||
|
||||
pub fn handles(&self) -> bool {
|
||||
self.all && self.anchors && self.handles
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct OverlayContext {
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
#[serde(skip)]
|
||||
#[specta(skip)]
|
||||
pub scene: Scene,
|
||||
pub size: DVec2,
|
||||
// The device pixel ratio is a property provided by the browser window and is the CSS pixel size divided by the physical monitor's pixel size.
|
||||
// It allows better pixel density of visualizations on high-DPI displays where the OS display scaling is not 100%, or where the browser is zoomed.
|
||||
pub device_pixel_ratio: f64,
|
||||
pub visibility_settings: OverlaysVisibilitySettings,
|
||||
}
|
||||
|
||||
// Manual implementations since Scene doesn't implement PartialEq or Debug
|
||||
impl PartialEq for OverlayContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.size == other.size && self.device_pixel_ratio == other.device_pixel_ratio && self.visibility_settings == other.visibility_settings
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OverlayContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OverlayContext")
|
||||
.field("scene", &"Scene { ... }")
|
||||
.field("size", &self.size)
|
||||
.field("device_pixel_ratio", &self.device_pixel_ratio)
|
||||
.field("visibility_settings", &self.visibility_settings)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Default implementation for Scene
|
||||
impl Default for OverlayContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scene: Scene::new(),
|
||||
size: DVec2::ZERO,
|
||||
device_pixel_ratio: 1.0,
|
||||
visibility_settings: OverlaysVisibilitySettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message hashing isn't used but is required by the message system macros
|
||||
impl core::hash::Hash for OverlayContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
impl OverlayContext {
|
||||
fn parse_color(color: &str) -> peniko::Color {
|
||||
let hex = color.trim_start_matches('#');
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
||||
let a = if hex.len() >= 8 { u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) } else { 255 };
|
||||
peniko::Color::from_rgba8(r, g, b, a)
|
||||
}
|
||||
|
||||
pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) {
|
||||
self.dashed_polygon(&quad.0, stroke_color, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&mut self, base: DVec2, direction: DVec2, size: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let normal = direction.perp();
|
||||
let top = base + direction * size;
|
||||
let edge1 = base + normal * size / 2.;
|
||||
let edge2 = base - normal * size / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(top.x, top.y));
|
||||
path.line_to(kurbo::Point::new(edge1.x, edge1.y));
|
||||
path.line_to(kurbo::Point::new(edge2.x, edge2.y));
|
||||
path.close_path();
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &path);
|
||||
}
|
||||
|
||||
pub fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
self.dashed_polygon(&quad.0, stroke_color, color_fill, dash_width, dash_gap_width, dash_offset);
|
||||
}
|
||||
|
||||
pub fn polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>) {
|
||||
self.dashed_polygon(polygon, stroke_color, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn dashed_polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
if polygon.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
let mut path = BezPath::new();
|
||||
if let Some(first) = polygon.last() {
|
||||
path.move_to(kurbo::Point::new(first.x.round() - 0.5, first.y.round() - 0.5));
|
||||
}
|
||||
|
||||
for point in polygon {
|
||||
path.line_to(kurbo::Point::new(point.x.round() - 0.5, point.y.round() - 0.5));
|
||||
}
|
||||
path.close_path();
|
||||
|
||||
if let Some(color_fill) = color_fill {
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path);
|
||||
}
|
||||
|
||||
let stroke_color = stroke_color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let mut stroke = kurbo::Stroke::new(1.0);
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap = dash_gap_width.unwrap_or(1.);
|
||||
stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]);
|
||||
}
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(stroke_color), None, &path);
|
||||
}
|
||||
|
||||
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>) {
|
||||
self.dashed_line(start, end, color, thickness, None, None, None)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
let transform = self.get_transform();
|
||||
|
||||
let start = start.round() - DVec2::splat(0.5);
|
||||
let end = end.round() - DVec2::splat(0.5);
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
path.line_to(kurbo::Point::new(end.x, end.y));
|
||||
|
||||
let mut stroke = kurbo::Stroke::new(thickness.unwrap_or(1.));
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap = dash_gap_width.unwrap_or(1.);
|
||||
stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]);
|
||||
}
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path);
|
||||
}
|
||||
|
||||
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
let transform = self.get_transform();
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
let circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
|
||||
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
|
||||
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
|
||||
}
|
||||
|
||||
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
|
||||
self.square(position, None, Some(color_fill), Some(color_stroke));
|
||||
}
|
||||
|
||||
fn get_transform(&self) -> kurbo::Affine {
|
||||
kurbo::Affine::scale(self.device_pixel_ratio)
|
||||
}
|
||||
|
||||
pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
let corner = position - DVec2::splat(size) / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
let rect = kurbo::Rect::new(corner.x, corner.y, corner.x + size, corner.y + size);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &rect);
|
||||
}
|
||||
|
||||
pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
|
||||
let size = 1.;
|
||||
let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
let corner = position - DVec2::splat(size) / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
let rect = kurbo::Rect::new(corner.x, corner.y, corner.x + size, corner.y + size);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect);
|
||||
}
|
||||
|
||||
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let position = position.round();
|
||||
|
||||
let transform = self.get_transform();
|
||||
let circle = kurbo::Circle::new((position.x, position.y), radius);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle);
|
||||
}
|
||||
|
||||
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
|
||||
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
|
||||
let step = (end_at - start_from) / segments as f64;
|
||||
let half_step = step / 2.;
|
||||
let factor = 4. / 3. * half_step.sin() / (1. + half_step.cos());
|
||||
|
||||
let mut path = BezPath::new();
|
||||
|
||||
for i in 0..segments {
|
||||
let start_angle = start_from + step * i as f64;
|
||||
let end_angle = start_angle + step;
|
||||
let start_vec = DVec2::from_angle(start_angle);
|
||||
let end_vec = DVec2::from_angle(end_angle);
|
||||
|
||||
let start = center + radius * start_vec;
|
||||
let end = center + radius * end_vec;
|
||||
|
||||
let handle_start = start + start_vec.perp() * radius * factor;
|
||||
let handle_end = end - end_vec.perp() * radius * factor;
|
||||
|
||||
if i == 0 {
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
}
|
||||
|
||||
path.curve_to(
|
||||
kurbo::Point::new(handle_start.x, handle_start.y),
|
||||
kurbo::Point::new(handle_end.x, handle_end.y),
|
||||
kurbo::Point::new(end.x, end.y),
|
||||
);
|
||||
}
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
|
||||
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
|
||||
let sign = scale.signum();
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None);
|
||||
self.circle(start, radius, fill_color, None);
|
||||
self.circle(start, radius * scale.abs(), fill_color, None);
|
||||
self.text(
|
||||
text,
|
||||
COLOR_OVERLAY_BLUE,
|
||||
None,
|
||||
DAffine2::from_translation(start + sign * DVec2::X * radius * (1. + scale.abs()) / 2.),
|
||||
2.,
|
||||
[Pivot::Middle, Pivot::End],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option<bool>) {
|
||||
const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.;
|
||||
const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
|
||||
const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.;
|
||||
const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.;
|
||||
const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
|
||||
const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
|
||||
const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
|
||||
const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
|
||||
|
||||
let Some(show_hover_ring) = show_compass_with_hover_ring else { return };
|
||||
|
||||
let transform = self.get_transform();
|
||||
let center = compass_center.round() - DVec2::splat(0.5);
|
||||
|
||||
// Hover ring
|
||||
if show_hover_ring {
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
|
||||
let circle = kurbo::Circle::new((center.x, center.y), HOVER_RING_CENTERLINE_RADIUS);
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(HOVER_RING_STROKE_WIDTH), transform, Self::parse_color(&fill_color), None, &circle);
|
||||
}
|
||||
|
||||
// Arrows
|
||||
for i in 0..4 {
|
||||
let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle);
|
||||
let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN };
|
||||
|
||||
let tip = center + direction * HOVER_RING_OUTER_RADIUS;
|
||||
let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.;
|
||||
|
||||
let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt();
|
||||
let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r);
|
||||
let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos);
|
||||
let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos);
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(tip.x, tip.y));
|
||||
path.line_to(kurbo::Point::new(side1.x, side1.y));
|
||||
path.line_to(kurbo::Point::new(base.x, base.y));
|
||||
path.line_to(kurbo::Point::new(side2.x, side2.y));
|
||||
path.close_path();
|
||||
|
||||
let color_parsed = Self::parse_color(color);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, color_parsed, None, &path);
|
||||
self.scene.stroke(&kurbo::Stroke::new(0.01), transform, color_parsed, None, &path);
|
||||
}
|
||||
|
||||
// Main ring
|
||||
let circle = kurbo::Circle::new((center.x, center.y), MAIN_RING_CENTERLINE_RADIUS);
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(MAIN_RING_STROKE_WIDTH), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle);
|
||||
}
|
||||
|
||||
pub fn pivot(&mut self, position: DVec2, angle: f64) {
|
||||
let uv = DVec2::from_angle(angle);
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
// Circle
|
||||
let circle = kurbo::Circle::new((x, y), PIVOT_DIAMETER / 2.);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &circle);
|
||||
|
||||
// Crosshair
|
||||
const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
|
||||
|
||||
let mut stroke = kurbo::Stroke::new(PIVOT_CROSSHAIR_THICKNESS);
|
||||
stroke = stroke.with_caps(kurbo::Cap::Round);
|
||||
|
||||
// Horizontal line
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y));
|
||||
path.line_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y));
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
|
||||
|
||||
// Vertical line
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x));
|
||||
path.line_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x));
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
|
||||
}
|
||||
|
||||
pub fn dowel_pin(&mut self, position: DVec2, angle: f64, color: Option<&str>) {
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_YELLOW_DULL);
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
// Draw the background circle with a white fill and colored outline
|
||||
let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color), None, &circle);
|
||||
|
||||
// Draw the two filled sectors using paths
|
||||
let mut path = BezPath::new();
|
||||
|
||||
// Top-left sector
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).cos();
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).sin();
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
||||
// Bottom-right sector
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).cos();
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).sin();
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color), None, &path);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
||||
/// Used by the Pen and Path tools to outline the path of the shape.
|
||||
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
|
||||
let mut last_point = None;
|
||||
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
|
||||
let move_to = last_point != Some(start_id);
|
||||
last_point = Some(end_id);
|
||||
|
||||
self.bezier_to_path(bezier, transform, move_to, &mut path);
|
||||
}
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
/// Used by the Pen tool in order to show how the bezier curve would look like.
|
||||
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
/// Used by the path tool segment mode in order to show the selected segments.
|
||||
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path);
|
||||
}
|
||||
|
||||
fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) {
|
||||
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
|
||||
if move_to {
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
}
|
||||
|
||||
match handles {
|
||||
bezier_rs::BezierHandles::Linear => path.line_to(kurbo::Point::new(end.x, end.y)),
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => path.quad_to(kurbo::Point::new(handle.x, handle.y), kurbo::Point::new(end.x, end.y)),
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(
|
||||
kurbo::Point::new(handle_start.x, handle_start.y),
|
||||
kurbo::Point::new(handle_end.x, handle_end.y),
|
||||
kurbo::Point::new(end.x, end.y),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
|
||||
for subpath in subpaths {
|
||||
let subpath = subpath.borrow();
|
||||
let mut curves = subpath.iter().peekable();
|
||||
|
||||
let Some(first) = curves.peek() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_point = transform.transform_point2(first.start());
|
||||
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
|
||||
|
||||
for curve in curves {
|
||||
match curve.handles {
|
||||
bezier_rs::BezierHandles::Linear => {
|
||||
let a = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
path.line_to(kurbo::Point::new(a.x, a.y));
|
||||
}
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
let a = transform.transform_point2(handle);
|
||||
let b = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.round() - DVec2::splat(0.5);
|
||||
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
|
||||
}
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let a = transform.transform_point2(handle_start);
|
||||
let b = transform.transform_point2(handle_end);
|
||||
let c = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.round() - DVec2::splat(0.5);
|
||||
let c = c.round() - DVec2::splat(0.5);
|
||||
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subpath.closed() {
|
||||
path.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
||||
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
||||
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
|
||||
|
||||
for target_type in target_types {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||
}
|
||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
if !subpaths.is_empty() {
|
||||
let path = self.push_path(subpaths.iter(), transform);
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(color), None, &path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
let path = self.push_path(subpaths, transform);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
|
||||
}
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
|
||||
// TODO: Implement pattern fill in Vello
|
||||
// For now, just fill with a semi-transparent version of the color
|
||||
let path = self.push_path(subpaths, transform);
|
||||
let semi_transparent_color = color.with_alpha(0.5);
|
||||
|
||||
self.scene.fill(
|
||||
peniko::Fill::NonZero,
|
||||
self.get_transform(),
|
||||
peniko::Color::from_rgba8(
|
||||
(semi_transparent_color.r() * 255.) as u8,
|
||||
(semi_transparent_color.g() * 255.) as u8,
|
||||
(semi_transparent_color.b() * 255.) as u8,
|
||||
(semi_transparent_color.a() * 255.) as u8,
|
||||
),
|
||||
None,
|
||||
&path,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_width(&self, _text: &str) -> f64 {
|
||||
// TODO: Implement proper text measurement in Vello
|
||||
0.
|
||||
}
|
||||
|
||||
pub fn text(&self, _text: &str, _font_color: &str, _background_color: Option<&str>, _transform: DAffine2, _padding: f64, _pivot: [Pivot; 2]) {
|
||||
// TODO: Implement text rendering in Vello
|
||||
}
|
||||
|
||||
pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) {
|
||||
if translation.x.abs() > 1e-3 {
|
||||
self.dashed_line(quad.top_left(), quad.top_right(), None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let width = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
};
|
||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
}
|
||||
|
||||
if translation.y.abs() > 1e-3 {
|
||||
self.dashed_line(quad.top_left(), quad.bottom_left(), None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let height = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
};
|
||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
self.text(height, COLOR_OVERLAY_BLUE, None, y_transform, 3., [height_pivot, Pivot::Middle]);
|
||||
}
|
||||
|
||||
if translation.x.abs() > 1e-3 && translation.y.abs() > 1e-3 {
|
||||
self.line(quad.top_right(), quad.bottom_right(), None, None);
|
||||
self.line(quad.bottom_left(), quad.bottom_right(), None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Pivot {
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
None,
|
||||
}
|
||||
@@ -3375,7 +3375,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_layer(&node, &[]))
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_locked(&node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
@@ -186,13 +186,18 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
aliases: &["graphene_core::ops::PercentageValueNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::math_nodes::coordinate_value::IDENTIFIER,
|
||||
node: graphene_std::math_nodes::vec_2_value::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::ops::CoordinateValueNode",
|
||||
"graphene_core::ops::ConstructVector2",
|
||||
"graphene_core::ops::Vector2ValueNode",
|
||||
"graphene_core::ops::CoordinateValueNode",
|
||||
"graphene_math_nodes::CoordinateValueNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::PositionToPointNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::math_nodes::color_value::IDENTIFIER,
|
||||
aliases: &["graphene_core::ops::ColorValueNode"],
|
||||
|
||||
@@ -107,15 +107,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
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| {
|
||||
graph_layer.horizontal_layer_flow().nth(1).map(|node_id| {
|
||||
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
|
||||
Some(format!("type:{}", output_type.nested_type()))
|
||||
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 is_modifiable = first_layer.is_some_and(|layer| {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
|
||||
});
|
||||
@@ -363,13 +363,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
for document_id in self.document_ids.iter() {
|
||||
let inspect_node = self.inspect_node_id();
|
||||
let _ = self.executor.submit_node_graph_evaluation(
|
||||
if let Ok(message) = self.executor.submit_node_graph_evaluation(
|
||||
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
||||
ipp.viewport_bounds.size().as_uvec2(),
|
||||
timing_information,
|
||||
inspect_node,
|
||||
true,
|
||||
);
|
||||
) {
|
||||
responses.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if self.active_document_mut().is_some() {
|
||||
@@ -568,8 +570,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(PortfolioMessage::CenterPastedLayers { layers });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,13 +704,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
if create_document {
|
||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||
});
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PasteSvg {
|
||||
@@ -733,13 +735,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
if create_document {
|
||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||
});
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PrevDocument => {
|
||||
@@ -846,11 +848,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
ignore_hash,
|
||||
);
|
||||
|
||||
if let Err(description) = result {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unable to update node graph".to_string(),
|
||||
description,
|
||||
});
|
||||
match result {
|
||||
Err(description) => {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unable to update node graph".to_string(),
|
||||
description,
|
||||
});
|
||||
}
|
||||
Ok(message) => responses.add(message),
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ToggleRulers => {
|
||||
@@ -883,6 +888,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents });
|
||||
}
|
||||
PortfolioMessage::UpdateVelloPreference => {
|
||||
let active = if cfg!(target_arch = "wasm32") { false } else { preferences.use_vello };
|
||||
responses.add(FrontendMessage::UpdateViewportHolePunch { active });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.persistent_data.use_vello = preferences.use_vello;
|
||||
}
|
||||
@@ -1019,9 +1026,6 @@ impl PortfolioMessageHandler {
|
||||
/text>"#
|
||||
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
|
||||
.to_string();
|
||||
responses.add(Message::EndBuffer {
|
||||
render_metadata: graphene_std::renderer::RenderMetadata::default(),
|
||||
});
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
|
||||
}
|
||||
result
|
||||
|
||||
@@ -53,7 +53,7 @@ impl From<String> for PanelType {
|
||||
"Layers" => PanelType::Layers,
|
||||
"Properties" => PanelType::Properties,
|
||||
"Spreadsheet" => PanelType::Spreadsheet,
|
||||
_ => panic!("Unknown panel type: {}", value),
|
||||
_ => panic!("Unknown panel type: {value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, Message
|
||||
pub use crate::utility_types::{DebugMessageTree, MessageData};
|
||||
// Message, MessageData, MessageDiscriminant, MessageHandler
|
||||
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
|
||||
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
|
||||
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
|
||||
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
|
||||
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
|
||||
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};
|
||||
pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler};
|
||||
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
|
||||
@@ -65,10 +67,12 @@ pub trait Responses {
|
||||
}
|
||||
|
||||
impl Responses for VecDeque<Message> {
|
||||
#[inline(always)]
|
||||
fn add(&mut self, message: impl Into<Message>) {
|
||||
self.push_back(message.into());
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn add_front(&mut self, message: impl Into<Message>) {
|
||||
self.push_front(message.into());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -123,6 +124,15 @@ impl ShapeGizmoHandlers {
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gizmo_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
match self {
|
||||
Self::Star(h) => h.mouse_cursor_icon(),
|
||||
Self::Polygon(h) => h.mouse_cursor_icon(),
|
||||
Self::Arc(h) => h.mouse_cursor_icon(),
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
|
||||
@@ -256,4 +266,12 @@ impl GizmoManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cursor icon to display when hovering or dragging a gizmo.
|
||||
///
|
||||
/// If a gizmo is active (hovered or being manipulated), it returns the cursor icon associated with that gizmo;
|
||||
/// otherwise, returns `None` to indicate the default crosshair cursor should be used.
|
||||
pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
self.active_shape_handler.as_ref().and_then(|h| h.gizmo_cursor_icon())
|
||||
}
|
||||
}
|
||||
|
||||
+17
-13
@@ -1,10 +1,9 @@
|
||||
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{arc_end_points, calculate_arc_text_transform, extract_arc_parameters, format_rounded};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
@@ -57,7 +56,7 @@ impl SweepAngleGizmo {
|
||||
self.handle_state == SweepAngleGizmoState::Dragging || self.handle_state == SweepAngleGizmoState::Snapped
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||
if self.handle_state == SweepAngleGizmoState::Inactive {
|
||||
let Some((start, end)) = arc_end_points(Some(layer), document) else { return };
|
||||
let Some((_, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
|
||||
@@ -89,8 +88,6 @@ impl SweepAngleGizmo {
|
||||
self.snap_angles = Self::calculate_snap_angles();
|
||||
|
||||
self.update_state(SweepAngleGizmoState::Hover);
|
||||
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +124,11 @@ impl SweepAngleGizmo {
|
||||
|
||||
// Depending on which endpoint is being dragged, draw guides relative to the static point
|
||||
let point = if self.endpoint == EndpointType::End { current_end } else { current_start };
|
||||
|
||||
// Draw the dashed line from center to drag start position
|
||||
overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
|
||||
|
||||
// Draw the angle, text and the bold line
|
||||
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
|
||||
}
|
||||
SweepAngleGizmoState::Snapped => {
|
||||
@@ -143,6 +145,9 @@ impl SweepAngleGizmo {
|
||||
// Draw lines from endpoints to the arc center
|
||||
overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
|
||||
// Draw the line from drag start to arc center
|
||||
overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +160,6 @@ impl SweepAngleGizmo {
|
||||
let final_vector = final_point - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let dash_radius = initial_point.distance(center);
|
||||
let bold_radius = final_point.distance(center);
|
||||
|
||||
let angle = initial_vector.angle_to(final_vector).to_degrees();
|
||||
@@ -170,7 +174,7 @@ impl SweepAngleGizmo {
|
||||
|
||||
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
|
||||
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, dash_radius, center, &text, transform);
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, center, &text, transform);
|
||||
}
|
||||
|
||||
pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
@@ -209,12 +213,11 @@ impl SweepAngleGizmo {
|
||||
let wrapped = new_sweep_angle % 360.;
|
||||
self.total_angle_delta = -wrapped;
|
||||
|
||||
// Remaining drag gets passed to the ending endpoint
|
||||
let rest_angle = angle_delta + wrapped;
|
||||
self.endpoint = EndpointType::End;
|
||||
|
||||
self.initial_sweep_angle = 360.;
|
||||
self.initial_start_angle = current_start_angle + rest_angle;
|
||||
self.initial_start_angle = current_start_angle;
|
||||
self.update_state(SweepAngleGizmoState::Snapped);
|
||||
|
||||
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses);
|
||||
}
|
||||
@@ -288,12 +291,13 @@ impl SweepAngleGizmo {
|
||||
}
|
||||
// Clamp sweep angle above 360°, switch to start
|
||||
() if new_sweep_angle > 360. => {
|
||||
let delta = angle_delta - (360. - current_sweep_angle);
|
||||
let delta = angle_delta - (360. - new_sweep_angle);
|
||||
let sign = -delta.signum();
|
||||
|
||||
self.total_angle_delta = angle_delta;
|
||||
self.total_angle_delta = angle_delta - (360. - new_sweep_angle);
|
||||
self.initial_sweep_angle = 360.;
|
||||
self.endpoint = EndpointType::Start;
|
||||
self.update_state(SweepAngleGizmoState::Snapped);
|
||||
|
||||
self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses);
|
||||
}
|
||||
@@ -339,7 +343,7 @@ impl SweepAngleGizmo {
|
||||
pub fn calculate_snap_angles() -> Vec<f64> {
|
||||
let mut snap_points = Vec::new();
|
||||
|
||||
for i in 0..8 {
|
||||
for i in 0..=8 {
|
||||
let snap_point = i as f64 * FRAC_PI_4;
|
||||
snap_points.push(snap_point.to_degrees());
|
||||
}
|
||||
|
||||
@@ -153,8 +153,9 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(PenToolMessage::RecalculateLatestPointsPosition);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::RecalculateLatestPointsPosition.into()],
|
||||
});
|
||||
}
|
||||
|
||||
/// Merge the `first_endpoint` with `second_endpoint`.
|
||||
|
||||
@@ -26,8 +26,8 @@ impl ArcGizmoHandler {
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandler for ArcGizmoHandler {
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position, responses);
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, _responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position);
|
||||
}
|
||||
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
@@ -74,6 +74,14 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
|
||||
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context);
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.sweep_angle_gizmo.hovered() || self.sweep_angle_gizmo.is_dragging_or_snapped() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.sweep_angle_gizmo.cleanup();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Ellipse {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Line {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, _, lock_angle, snap_angle] = modifier;
|
||||
let [center, snap_angle, lock_angle] = modifier;
|
||||
|
||||
shape_tool_data.line_data.drag_current = ipp.mouse.position;
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ impl ShapeGizmoHandler for PolygonGizmoHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
@@ -112,7 +124,7 @@ impl Polygon {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Rectangle {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::ShapeToolData;
|
||||
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -74,7 +75,7 @@ impl ShapeType {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ShapeToolModifierKey = [Key; 4];
|
||||
pub type ShapeToolModifierKey = [Key; 3];
|
||||
|
||||
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
|
||||
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
|
||||
@@ -127,6 +128,8 @@ pub trait ShapeGizmoHandler {
|
||||
///
|
||||
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
|
||||
fn cleanup(&mut self);
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon>;
|
||||
}
|
||||
|
||||
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
|
||||
|
||||
@@ -90,6 +90,18 @@ impl ShapeGizmoHandler for StarGizmoHandler {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -114,7 +126,7 @@ impl Star {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct BrushTool {
|
||||
}
|
||||
|
||||
pub struct BrushOptions {
|
||||
legacy_warning_was_shown: bool,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
@@ -41,7 +40,6 @@ pub struct BrushOptions {
|
||||
impl Default for BrushOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
legacy_warning_was_shown: false,
|
||||
diameter: DEFAULT_BRUSH_SIZE,
|
||||
hardness: 0.,
|
||||
flow: 100.,
|
||||
@@ -79,7 +77,6 @@ pub enum BrushToolMessageOptionsUpdate {
|
||||
Hardness(f64),
|
||||
Spacing(f64),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
NoDisplayLegacyWarning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -224,7 +221,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
|
||||
self.options.color.primary_working_color = primary;
|
||||
self.options.color.secondary_working_color = secondary;
|
||||
}
|
||||
BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning => self.options.legacy_warning_was_shown = true,
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
@@ -322,20 +318,6 @@ impl Fsm for BrushToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
if !tool_options.legacy_warning_was_shown {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unsupported tool".into(),
|
||||
description: "
|
||||
The current Brush tool is a legacy feature with\n\
|
||||
significant quality and performance limitations.\n\
|
||||
It will be replaced soon by a new implementation.\n\
|
||||
"
|
||||
.trim()
|
||||
.into(),
|
||||
});
|
||||
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning));
|
||||
}
|
||||
|
||||
let ToolMessage::Brush(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
|
||||
@@ -383,8 +365,9 @@ impl Fsm for BrushToolFsmState {
|
||||
else {
|
||||
new_brush_layer(document, responses);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(BrushToolMessage::DragStart);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![BrushToolMessage::DragStart.into()],
|
||||
});
|
||||
BrushToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,9 +251,12 @@ impl Fsm for FreehandToolFsmState {
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
let defered_responses = &mut VecDeque::new();
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, defered_responses);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: defered_responses.drain(..).collect(),
|
||||
});
|
||||
tool_data.layer = Some(layer);
|
||||
|
||||
FreehandToolFsmState::Drawing
|
||||
|
||||
@@ -2239,7 +2239,9 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.snapping_axis = None;
|
||||
tool_data.sliding_point_info = None;
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
if drag_occurred || extend_selection {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.opposite_handle_position = None;
|
||||
@@ -2292,7 +2294,9 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PathToolMessage::SelectedPointUpdated.into()],
|
||||
});
|
||||
}
|
||||
|
||||
return PathToolFsmState::Ready;
|
||||
|
||||
@@ -1257,10 +1257,10 @@ impl PenToolData {
|
||||
self.prior_segments = None;
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
|
||||
// This causes the following message to be run only after the next graph evaluation runs and the transforms are updated
|
||||
responses.add(Message::StartBuffer);
|
||||
// It is necessary to defer this until the transform of the layer can be accurately computed (quite hacky)
|
||||
responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::AddPointLayerPosition { layer, viewport }.into()],
|
||||
});
|
||||
}
|
||||
|
||||
/// Perform extension of an existing path
|
||||
@@ -1721,9 +1721,9 @@ impl Fsm for PenToolFsmState {
|
||||
let next_point = tool_data.next_point;
|
||||
let start = latest_point.id;
|
||||
|
||||
if let Some(layer) = layer {
|
||||
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
|
||||
|
||||
if let Some(layer) = layer
|
||||
&& let Some(mut vector_data) = document.network_interface.compute_modified_vector(layer)
|
||||
{
|
||||
let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| {
|
||||
vector_data.point_domain.position_from_id(id).map_or(false, |pos| {
|
||||
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
|
||||
|
||||
@@ -335,7 +335,7 @@ pub struct ShapeToolData {
|
||||
current_shape: ShapeType,
|
||||
|
||||
// Gizmos
|
||||
gizmo_manger: GizmoManager,
|
||||
gizmo_manager: GizmoManager,
|
||||
}
|
||||
|
||||
impl ShapeToolData {
|
||||
@@ -351,6 +351,23 @@ impl ShapeToolData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_cage_mouse_icon(&mut self, input: &InputPreprocessorMessageHandler) -> MouseCursorIcon {
|
||||
let dragging_bounds = self
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
|
||||
.is_some();
|
||||
|
||||
self.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
|
||||
let cursor_icon = bounds.get_cursor(input, true, dragging_bounds, Some(self.skew_edge));
|
||||
if cursor_icon == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor_icon }
|
||||
})
|
||||
}
|
||||
|
||||
fn shape_tool_modifier_keys() -> [Key; 3] {
|
||||
[Key::Alt, Key::Shift, Key::Control]
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for ShapeToolFsmState {
|
||||
@@ -388,30 +405,34 @@ impl Fsm for ShapeToolFsmState {
|
||||
.indicator_pos()
|
||||
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
|
||||
.unwrap_or(input.mouse.position);
|
||||
let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds);
|
||||
|
||||
if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.handle_actions(mouse_position, document, responses);
|
||||
tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
tool_data.gizmo_manager.handle_actions(mouse_position, document, responses);
|
||||
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
tool_data.gizmo_manager.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
|
||||
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
|
||||
let hovering_over_gizmo = tool_data.gizmo_manger.hovering_over_gizmo();
|
||||
let hovering_over_gizmo = tool_data.gizmo_manager.hovering_over_gizmo();
|
||||
|
||||
if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
|
||||
if !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
|
||||
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
}
|
||||
|
||||
if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) {
|
||||
transform_cage_overlays(document, tool_data, &mut overlay_context);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: tool_data.cursor });
|
||||
}
|
||||
|
||||
if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
anchor_overlays(document, &mut overlay_context);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
} else if matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
Line::overlays(document, tool_data, &mut overlay_context);
|
||||
|
||||
@@ -433,7 +454,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. });
|
||||
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
|
||||
if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating && !hovering_over_gizmo) {
|
||||
if is_skewing || (dragging_bounds && is_near_square && !hovering_over_gizmo) {
|
||||
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
|
||||
}
|
||||
if !is_skewing && dragging_bounds && !hovering_over_gizmo {
|
||||
@@ -442,6 +463,11 @@ impl Fsm for ShapeToolFsmState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or_else(|| tool_data.transform_cage_mouse_icon(input));
|
||||
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
|
||||
@@ -562,8 +588,16 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
tool_data.line_data.drag_current = mouse_pos;
|
||||
|
||||
if tool_data.gizmo_manger.handle_click() {
|
||||
if tool_data.gizmo_manager.handle_click() && !input.keyboard.key(Key::Accel) {
|
||||
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
// Send a PointerMove message to refresh the cursor icon
|
||||
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys()));
|
||||
|
||||
return ShapeToolFsmState::ModifyingGizmo;
|
||||
}
|
||||
|
||||
@@ -584,17 +618,31 @@ impl Fsm for ShapeToolFsmState {
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, None);
|
||||
|
||||
if !input.keyboard.key(Key::Control) {
|
||||
// Helper function to update cursor and send pointer move message
|
||||
let update_cursor_and_pointer = |tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>| {
|
||||
let cursor = tool_data.transform_cage_mouse_icon(input);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys()));
|
||||
};
|
||||
|
||||
match (resize, rotate, skew) {
|
||||
(true, false, false) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::ResizingBounds;
|
||||
}
|
||||
(false, true, false) => {
|
||||
tool_data.data.drag_start = mouse_pos;
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::RotatingBounds;
|
||||
}
|
||||
(false, false, true) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::SkewingBounds { skew: Key::Control };
|
||||
}
|
||||
_ => {}
|
||||
@@ -624,29 +672,33 @@ impl Fsm for ShapeToolFsmState {
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
|
||||
responses.add(Message::StartBuffer);
|
||||
let defered_responses = &mut VecDeque::new();
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => {
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
defered_responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Line => {
|
||||
tool_data.line_data.weight = tool_options.line_weight;
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
}
|
||||
}
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
tool_data.data.layer = Some(layer);
|
||||
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: defered_responses.drain(..).collect(),
|
||||
});
|
||||
|
||||
ShapeToolFsmState::Drawing(tool_data.current_shape)
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => {
|
||||
@@ -682,8 +734,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses);
|
||||
tool_data.gizmo_manager.handle_update(tool_data.data.drag_start, document, input, responses);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
@@ -754,7 +805,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
if cursor == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor }
|
||||
});
|
||||
|
||||
if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && !all_selected_layers_line {
|
||||
if tool_data.cursor != cursor {
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
@@ -793,7 +844,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
input.mouse.finish_transaction(tool_data.data.drag_start, responses);
|
||||
tool_data.data.cleanup(responses);
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
tool_data.gizmo_manager.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
@@ -818,12 +869,13 @@ impl Fsm for ShapeToolFsmState {
|
||||
tool_data.data.cleanup(responses);
|
||||
tool_data.line_data.dragging_endpoint = None;
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
tool_data.gizmo_manager.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
tool_data.cursor = MouseCursorIcon::Crosshair;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
|
||||
ShapeToolFsmState::Ready(tool_data.current_shape)
|
||||
|
||||
@@ -360,8 +360,6 @@ impl Fsm for SplineToolFsmState {
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_data.current_layer = Some(layer);
|
||||
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
SplineToolFsmState::Drawing
|
||||
}
|
||||
(SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => {
|
||||
|
||||
@@ -385,20 +385,25 @@ impl TextToolData {
|
||||
parent: document.new_layer_parent(true),
|
||||
insert_index: 0,
|
||||
});
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() {
|
||||
Fill::Solid(editing_text.color.unwrap().to_gamma_srgb())
|
||||
} else {
|
||||
Fill::None
|
||||
},
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![
|
||||
GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() {
|
||||
Fill::Solid(editing_text.color.unwrap().to_gamma_srgb())
|
||||
} else {
|
||||
Fill::None
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
});
|
||||
self.editing_text = Some(editing_text);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
use graph_craft::document::value::{RenderOutput, TaggedValue};
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, generate_uuid};
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graphene_std::application_io::TimingInformation;
|
||||
@@ -29,7 +29,6 @@ pub struct ExecutionRequest {
|
||||
render_config: RenderConfig,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ExecutionResponse {
|
||||
execution_id: u64,
|
||||
result: Result<TaggedValue, String>,
|
||||
@@ -46,7 +45,6 @@ pub struct CompilationResponse {
|
||||
node_graph_errors: GraphErrors,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum NodeGraphUpdate {
|
||||
ExecutionResponse(ExecutionResponse),
|
||||
CompilationResponse(CompilationResponse),
|
||||
@@ -56,6 +54,7 @@ pub enum NodeGraphUpdate {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NodeGraphExecutor {
|
||||
runtime_io: NodeRuntimeIO,
|
||||
current_execution_id: u64,
|
||||
futures: HashMap<u64, ExecutionContext>,
|
||||
node_graph_hash: u64,
|
||||
old_inspect_node: Option<NodeId>,
|
||||
@@ -78,13 +77,15 @@ impl NodeGraphExecutor {
|
||||
futures: Default::default(),
|
||||
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
|
||||
node_graph_hash: 0,
|
||||
current_execution_id: 0,
|
||||
old_inspect_node: None,
|
||||
};
|
||||
(node_runtime, node_executor)
|
||||
}
|
||||
/// Execute the network by flattening it and creating a borrow stack.
|
||||
fn queue_execution(&self, render_config: RenderConfig) -> u64 {
|
||||
let execution_id = generate_uuid();
|
||||
fn queue_execution(&mut self, render_config: RenderConfig) -> u64 {
|
||||
let execution_id = self.current_execution_id;
|
||||
self.current_execution_id += 1;
|
||||
let request = ExecutionRequest { execution_id, render_config };
|
||||
self.runtime_io.send(GraphRuntimeRequest::ExecutionRequest(request)).expect("Failed to send generation request");
|
||||
|
||||
@@ -105,7 +106,7 @@ impl NodeGraphExecutor {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn update_node_graph_instrumented(&mut self, document: &mut DocumentMessageHandler) -> Result<Instrumented, String> {
|
||||
// We should always invalidate the cache.
|
||||
self.node_graph_hash = generate_uuid();
|
||||
self.node_graph_hash = crate::application::generate_uuid();
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
let instrumented = Instrumented::new(&mut network);
|
||||
|
||||
@@ -132,7 +133,7 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
|
||||
/// Adds an evaluate request for whatever current network is cached.
|
||||
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, time: TimingInformation) -> Result<(), String> {
|
||||
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, time: TimingInformation) -> Result<Message, String> {
|
||||
let render_config = RenderConfig {
|
||||
viewport: Footprint {
|
||||
transform: document.metadata().document_to_viewport,
|
||||
@@ -154,7 +155,7 @@ impl NodeGraphExecutor {
|
||||
|
||||
self.futures.insert(execution_id, ExecutionContext { export_config: None });
|
||||
|
||||
Ok(())
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex(execution_id).into())
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
@@ -165,11 +166,9 @@ impl NodeGraphExecutor {
|
||||
time: TimingInformation,
|
||||
inspect_node: Option<NodeId>,
|
||||
ignore_hash: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<Message, String> {
|
||||
self.update_node_graph(document, inspect_node, ignore_hash)?;
|
||||
self.submit_current_node_graph_evaluation(document, viewport_resolution, time)?;
|
||||
|
||||
Ok(())
|
||||
self.submit_current_node_graph_evaluation(document, viewport_resolution, time)
|
||||
}
|
||||
|
||||
/// Evaluates a node graph for export
|
||||
@@ -280,6 +279,7 @@ impl NodeGraphExecutor {
|
||||
} else {
|
||||
self.process_node_graph_output(node_graph_output, transform, responses)?
|
||||
}
|
||||
responses.add(DeferMessage::TriggerGraphRun(execution_id));
|
||||
|
||||
// Update the spreadsheet on the frontend using the value of the inspect result.
|
||||
if self.old_inspect_node.is_some() {
|
||||
@@ -364,6 +364,7 @@ impl NodeGraphExecutor {
|
||||
);
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
}
|
||||
graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {}
|
||||
_ => {
|
||||
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
|
||||
}
|
||||
@@ -384,9 +385,22 @@ impl NodeGraphExecutor {
|
||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||
}
|
||||
};
|
||||
responses.add(Message::EndBuffer {
|
||||
render_metadata: render_output_metadata,
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_output_metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
responses.add(DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
});
|
||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
@@ -8,7 +8,7 @@ use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::Context;
|
||||
use graphene_std::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::instances::Instance;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::renderer::{GraphicElementRendered, RenderParams, SvgRender};
|
||||
@@ -16,7 +16,7 @@ use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
|
||||
use graphene_std::text::FontCache;
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
|
||||
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
|
||||
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
|
||||
use interpreted_executor::util::wrap_network_in_scope;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -131,12 +131,12 @@ impl NodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
pub async fn run(&mut self) -> Option<ImageTexture> {
|
||||
if self.editor_api.application_io.is_none() {
|
||||
self.editor_api = WasmEditorApi {
|
||||
#[cfg(not(test))]
|
||||
#[cfg(all(not(test), target_arch = "wasm32"))]
|
||||
application_io: Some(WasmApplicationIo::new().await.into()),
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, not(target_arch = "wasm32")))]
|
||||
application_io: Some(WasmApplicationIo::new_offscreen().await.into()),
|
||||
font_cache: self.editor_api.font_cache.clone(),
|
||||
node_graph_message_sender: Box::new(self.sender.clone()),
|
||||
@@ -213,6 +213,16 @@ impl NodeRuntime {
|
||||
// Resolve the result from the inspection by accessing the monitor node
|
||||
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
|
||||
|
||||
let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||
data: RenderOutputType::Texture(texture),
|
||||
..
|
||||
})) = &result
|
||||
{
|
||||
// We can early return becaus we know that there is at most one execution request and it will always be handled last
|
||||
Some(texture.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.sender.send_execution_response(ExecutionResponse {
|
||||
execution_id,
|
||||
result,
|
||||
@@ -221,9 +231,11 @@ impl NodeRuntime {
|
||||
vector_modify: self.vector_modify.clone(),
|
||||
inspect_result,
|
||||
});
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
|
||||
@@ -382,18 +394,30 @@ pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any +
|
||||
Err(IntrospectError::RuntimeNotReady)
|
||||
}
|
||||
|
||||
pub async fn run_node_graph() -> bool {
|
||||
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return false };
|
||||
pub async fn run_node_graph() -> (bool, Option<ImageTexture>) {
|
||||
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) };
|
||||
if let Some(ref mut runtime) = runtime.as_mut() {
|
||||
runtime.run().await;
|
||||
return (true, runtime.run().await);
|
||||
}
|
||||
true
|
||||
(false, None)
|
||||
}
|
||||
|
||||
pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
|
||||
let mut node_runtime = NODE_RUNTIME.lock();
|
||||
node_runtime.replace(runtime)
|
||||
}
|
||||
pub async fn replace_application_io(application_io: WasmApplicationIo) {
|
||||
let mut node_runtime = NODE_RUNTIME.lock();
|
||||
if let Some(node_runtime) = &mut *node_runtime {
|
||||
node_runtime.editor_api = WasmEditorApi {
|
||||
font_cache: node_runtime.editor_api.font_cache.clone(),
|
||||
application_io: Some(application_io.into()),
|
||||
node_graph_message_sender: Box::new(node_runtime.sender.clone()),
|
||||
editor_preferences: Box::new(node_runtime.editor_preferences.clone()),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
/// Which node is inspected and which monitor node is used (if any) for the current execution
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -403,22 +427,14 @@ struct InspectState {
|
||||
}
|
||||
/// The resulting value from the temporary inspected during execution
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct InspectResult {
|
||||
#[cfg(not(feature = "decouple-execution"))]
|
||||
introspected_data: Option<Arc<dyn std::any::Any + Send + Sync + 'static>>,
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
introspected_data: Option<TaggedValue>,
|
||||
pub inspect_node: NodeId,
|
||||
}
|
||||
|
||||
impl InspectResult {
|
||||
pub fn take_data(&mut self) -> Option<Arc<dyn std::any::Any + Send + Sync + 'static>> {
|
||||
#[cfg(not(feature = "decouple-execution"))]
|
||||
return self.introspected_data.clone();
|
||||
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
return self.introspected_data.take().map(|value| value.to_any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,8 +479,6 @@ impl InspectState {
|
||||
fn access(&self, executor: &DynamicExecutor) -> Option<InspectResult> {
|
||||
let introspected_data = executor.introspect(&[self.monitor_node]).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok();
|
||||
// TODO: Consider displaying the error instead of ignoring it
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
let introspected_data = introspected_data.as_ref().and_then(|data| TaggedValue::try_from_std_any_ref(data).ok());
|
||||
|
||||
Some(InspectResult {
|
||||
inspect_node: self.inspect_node,
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
use super::*;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
// Invoke with arguments (default)
|
||||
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
|
||||
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
|
||||
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name="invoke")]
|
||||
async fn invoke_without_arg(cmd: &str) -> JsValue;
|
||||
}
|
||||
|
||||
/// Handles communication with the NodeRuntime, either locally or via Tauri
|
||||
/// Handles communication with the NodeRuntime
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRuntimeIO {
|
||||
// Send to
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
sender: Sender<GraphRuntimeRequest>,
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
sender: Sender<NodeGraphUpdate>,
|
||||
receiver: Receiver<NodeGraphUpdate>,
|
||||
}
|
||||
|
||||
@@ -31,25 +18,13 @@ impl Default for NodeRuntimeIO {
|
||||
impl NodeRuntimeIO {
|
||||
/// Creates a new NodeRuntimeIO instance
|
||||
pub fn new() -> Self {
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
{
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
futures::executor::block_on(replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)));
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
futures::executor::block_on(replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)));
|
||||
|
||||
Self {
|
||||
sender: request_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
Self {
|
||||
sender: response_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
Self {
|
||||
sender: request_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
@@ -59,44 +34,11 @@ impl NodeRuntimeIO {
|
||||
|
||||
/// Sends a message to the NodeRuntime
|
||||
pub fn send(&self, message: GraphRuntimeRequest) -> Result<(), String> {
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
{
|
||||
self.sender.send(message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let serialized = ron::to_string(&message).map_err(|e| e.to_string()).unwrap();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let js_message = create_message_object(&serialized);
|
||||
invoke("runtime_message", js_message).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
self.sender.send(message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Receives any pending updates from the NodeRuntime
|
||||
pub fn receive(&self) -> impl Iterator<Item = NodeGraphUpdate> + use<'_> {
|
||||
// TODO: This introduces extra latency
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let sender = self.sender.clone();
|
||||
// In the Tauri case, responses are handled separately via poll_node_runtime_updates
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let messages = invoke_without_arg("poll_node_graph").await;
|
||||
let vec: Vec<_> = ron::from_str(&messages.as_string().unwrap()).unwrap();
|
||||
for message in vec {
|
||||
sender.send(message).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
self.receiver.try_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
pub fn create_message_object(message: &str) -> JsValue {
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&obj, &JsValue::from_str("message"), &JsValue::from_str(message)).unwrap();
|
||||
obj.into()
|
||||
}
|
||||
|
||||
+5
-7
@@ -1,8 +1,6 @@
|
||||
# Overview of `/frontend/`
|
||||
|
||||
The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Svelte](https://svelte.dev/) framework. The backend is written in Rust and compiled to WebAssembly (WASM) to be run in the browser alongside the JS code.
|
||||
|
||||
For lack of other options, the frontend is currently written as a web app. Maintaining web compatibility will always be a requirement, but the long-term plan is to port this code to a Rust-based native GUI framework, either written by the Rust community or created by our project if necessary. As a medium-term compromise, we may wrap the web-based frontend in a desktop webview windowing solution like Electron (probably not) or [Tauri](https://tauri.app/) (probably).
|
||||
The Graphite frontend is a web app that provides the presentation for the editor. It displays the GUI based on state from the backend and provides users with interactive widgets that send updates to the backend, which is the source of truth for state information. The frontend is built out of reactive components using the [Svelte](https://svelte.dev/) framework. The backend is written in Rust and compiled to WebAssembly (Wasm) to be run in the browser alongside the JS code.
|
||||
|
||||
## Bundled assets: `assets/`
|
||||
|
||||
@@ -18,15 +16,15 @@ Source code for the web app in the form of Svelte components and [TypeScript](ht
|
||||
|
||||
## WebAssembly wrapper: `wasm/`
|
||||
|
||||
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the WASM module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack).
|
||||
Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the Wasm module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack).
|
||||
|
||||
## ESLint configurations: `.eslintrc.js`
|
||||
## ESLint configurations: `.eslintrc.cjs`
|
||||
|
||||
[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter.
|
||||
|
||||
## npm ecosystem packages: `package.json`
|
||||
|
||||
While we don't use Node.js as a JS-based server, we do have to rely on its wide ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of Node.js. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vite, ESLint, Prettier, wasm-pack, and [Sass](https://sass-lang.com/)) that run in your console during the build process.
|
||||
While we don't use Node.js as a JS-based server, we do rely on its ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of [Node.js](https://nodejs.org/en/download). Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vite, ESLint, Prettier, and [Sass](https://sass-lang.com/)) that run in your terminal during the build process.
|
||||
|
||||
## npm package installed versions: `package-lock.json`
|
||||
|
||||
@@ -36,6 +34,6 @@ Specifies the exact versions of packages installed in the npm dependency tree. W
|
||||
|
||||
Basic configuration options for the TypeScript build tool to do its job in our repository.
|
||||
|
||||
## Vite configurations: `vite.config.js`
|
||||
## Vite configurations: `vite.config.ts`
|
||||
|
||||
We use the [Vite](https://vitejs.dev/) bundler/build system. This file is where we configure Vite to set up plugins (like the third-party license checker/generator). Part of the license checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code.
|
||||
|
||||
+7
-1
@@ -21,11 +21,17 @@
|
||||
<noscript>JavaScript is required</noscript>
|
||||
<style>
|
||||
body {
|
||||
background: #222;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"",
|
||||
"---------- BUILDS ----------": "",
|
||||
"build-dev": "npm run wasm:build-dev && vite build",
|
||||
"build-native": "npm run native:build-dev && vite build",
|
||||
"build-profiling": "npm run wasm:build-profiling && vite build",
|
||||
"build": "npm run wasm:build-production && vite build",
|
||||
"---------- UTILITIES ----------": "",
|
||||
@@ -19,6 +20,7 @@
|
||||
"lint-fix": "eslint . --fix && tsc --noEmit",
|
||||
"---------- INTERNAL ----------": "",
|
||||
"setup": "node package-installer.js",
|
||||
"native:build-dev": "wasm-pack build ./wasm --dev --target=web --features native",
|
||||
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
|
||||
"wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web",
|
||||
"wasm:build-production": "wasm-pack build ./wasm --release --target=web",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { createLocalizationManager } from "@graphite/io-managers/localization";
|
||||
import { createPanicManager } from "@graphite/io-managers/panic";
|
||||
import { createPersistenceManager } from "@graphite/io-managers/persistence";
|
||||
import { createAppWindowState } from "@graphite/state-providers/app-window";
|
||||
import { createDialogState } from "@graphite/state-providers/dialog";
|
||||
import { createDocumentState } from "@graphite/state-providers/document";
|
||||
import { createFontsState } from "@graphite/state-providers/fonts";
|
||||
@@ -36,6 +37,8 @@
|
||||
setContext("nodeGraph", nodeGraph);
|
||||
let portfolio = createPortfolioState(editor);
|
||||
setContext("portfolio", portfolio);
|
||||
let appWindow = createAppWindowState(editor);
|
||||
setContext("appWindow", appWindow);
|
||||
|
||||
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
||||
createClipboardManager(editor);
|
||||
@@ -58,10 +61,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<MainWindow />
|
||||
<MainWindow platform={$appWindow.platform} maximized={$appWindow.maximized} viewportHolePunch={$appWindow.viewportHolePunch} />
|
||||
|
||||
<style lang="scss" global>
|
||||
// Disable the spinning loading indicator
|
||||
body::before,
|
||||
body::after {
|
||||
content: none !important;
|
||||
}
|
||||
@@ -206,10 +210,16 @@
|
||||
height: 100%;
|
||||
background: var(--color-2-mildblack);
|
||||
overscroll-behavior: none;
|
||||
-webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
||||
-webkit-user-select: none; // Still required by Safari as of 2025
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
html:has(body > .viewport-hole-punch),
|
||||
body:has(> .viewport-hole-punch) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
// The default value of `auto` from the CSS spec is a footgun with flexbox layouts:
|
||||
// https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size
|
||||
* {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
UpdateMouseCursor,
|
||||
isWidgetSpanRow,
|
||||
} from "@graphite/messages";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
@@ -34,6 +35,7 @@
|
||||
let viewport: HTMLDivElement | undefined;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const document = getContext<DocumentState>("document");
|
||||
|
||||
// Interactive text editing
|
||||
@@ -239,8 +241,8 @@
|
||||
`.trim();
|
||||
|
||||
if (!rasterizedCanvas) {
|
||||
rasterizedCanvas = await rasterizeSVGCanvas(svg, width * dpiFactor, height * dpiFactor, "image/png");
|
||||
rasterizedContext = rasterizedCanvas.getContext("2d") || undefined;
|
||||
rasterizedCanvas = await rasterizeSVGCanvas(svg, width * dpiFactor, height * dpiFactor);
|
||||
rasterizedContext = rasterizedCanvas.getContext("2d", { willReadFrequently: true }) || undefined;
|
||||
}
|
||||
if (!rasterizedContext) return undefined;
|
||||
|
||||
@@ -514,13 +516,13 @@
|
||||
<RulerInput origin={rulerOrigin.x} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Horizontal" bind:this={rulerHorizontal} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
<LayoutRow class="viewport-container-inner">
|
||||
<LayoutRow class="viewport-container-inner-1">
|
||||
{#if rulersVisible}
|
||||
<LayoutCol class="ruler-or-scrollbar">
|
||||
<RulerInput origin={rulerOrigin.y} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Vertical" bind:this={rulerVertical} />
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
<LayoutCol class="viewport-container-inner" styles={{ cursor: canvasCursor }}>
|
||||
<LayoutCol class="viewport-container-inner-2" styles={{ cursor: canvasCursor }} data-viewport-container>
|
||||
{#if cursorEyedropper}
|
||||
<EyedropperPreview
|
||||
colorChoice={cursorEyedropperPreviewColorChoice}
|
||||
@@ -531,25 +533,27 @@
|
||||
y={cursorTop}
|
||||
/>
|
||||
{/if}
|
||||
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
|
||||
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
|
||||
{@html artworkSvg}
|
||||
</svg>
|
||||
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
|
||||
{#if showTextInput}
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
|
||||
{/if}
|
||||
{#if !$appWindow.viewportHolePunch}
|
||||
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
|
||||
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
|
||||
{@html artworkSvg}
|
||||
</svg>
|
||||
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
|
||||
{#if showTextInput}
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
|
||||
{/if}
|
||||
</div>
|
||||
<canvas
|
||||
class="overlays"
|
||||
width={canvasWidthScaledRoundedToEven}
|
||||
height={canvasHeightScaledRoundedToEven}
|
||||
style:width={canvasWidthCSS}
|
||||
style:height={canvasHeightCSS}
|
||||
data-overlays-canvas
|
||||
>
|
||||
</canvas>
|
||||
</div>
|
||||
<canvas
|
||||
class="overlays"
|
||||
width={canvasWidthScaledRoundedToEven}
|
||||
height={canvasHeightScaledRoundedToEven}
|
||||
style:width={canvasWidthCSS}
|
||||
style:height={canvasHeightCSS}
|
||||
data-overlays-canvas
|
||||
>
|
||||
</canvas>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>
|
||||
<Graph />
|
||||
</div>
|
||||
@@ -593,7 +597,8 @@
|
||||
.control-bar {
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
margin: 0 4px;
|
||||
padding: 0 4px; // Padding (instead of margin) is needed for the viewport hole punch on desktop
|
||||
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
|
||||
|
||||
.spacer {
|
||||
min-width: 40px;
|
||||
@@ -632,6 +637,7 @@
|
||||
.tool-shelf {
|
||||
flex: 0 0 auto;
|
||||
justify-content: space-between;
|
||||
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
|
||||
|
||||
.tools {
|
||||
flex: 0 1 auto;
|
||||
@@ -713,6 +719,7 @@
|
||||
|
||||
.ruler-or-scrollbar {
|
||||
flex: 0 0 auto;
|
||||
background: var(--color-3-darkgray); // Needed for the viewport hole punch on desktop
|
||||
}
|
||||
|
||||
.ruler-corner {
|
||||
@@ -743,7 +750,8 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.viewport-container-inner {
|
||||
.viewport-container-inner-1,
|
||||
.viewport-container-inner-2 {
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts" context="module">
|
||||
export type ApplicationPlatform = "Windows" | "Mac" | "Linux" | "Web";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { AppWindowPlatform } from "@graphite/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import StatusBar from "@graphite/components/window/status-bar/StatusBar.svelte";
|
||||
import TitleBar from "@graphite/components/window/title-bar/TitleBar.svelte";
|
||||
import Workspace from "@graphite/components/window/workspace/Workspace.svelte";
|
||||
|
||||
let platform: ApplicationPlatform = "Web";
|
||||
let maximized: true;
|
||||
export let platform: AppWindowPlatform;
|
||||
export let maximized: boolean;
|
||||
export let viewportHolePunch: boolean;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="main-window">
|
||||
<LayoutCol class="main-window" classes={{ "viewport-hole-punch": viewportHolePunch }}>
|
||||
<TitleBar {platform} {maximized} />
|
||||
|
||||
<Workspace />
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script lang="ts" context="module">
|
||||
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, type AppWindowPlatform, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
@@ -17,7 +13,7 @@
|
||||
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
|
||||
import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte";
|
||||
|
||||
export let platform: Platform;
|
||||
export let platform: AppWindowPlatform;
|
||||
export let maximized: boolean;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
@@ -73,7 +69,7 @@
|
||||
<!-- Menu bar (or on Mac: window buttons) -->
|
||||
<LayoutRow class="left">
|
||||
{#if platform === "Mac"}
|
||||
<WindowButtonsMac {maximized} />
|
||||
<WindowButtonsMac />
|
||||
{:else}
|
||||
{#each entries as entry}
|
||||
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} action={entry.action} flush={true} />
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
export let maximized = false;
|
||||
const editor = getContext<Editor>("editor");
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-buttons-mac">
|
||||
<div class="close" title="Close" />
|
||||
<div class="minimize" title={maximized ? "Minimize" : "Maximize"} />
|
||||
<div class="zoom" title="Zoom" />
|
||||
<div class="close" on:click={() => editor.handle.appWindowClose()} />
|
||||
<div class="minimize" on:click={() => editor.handle.appWindowMinimize()} />
|
||||
<div class="zoom" on:click={() => editor.handle.appWindowMaximize()} />
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const fullscreen = getContext<FullscreenState>("fullscreen");
|
||||
|
||||
$: requestFullscreenHotkeys = fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked;
|
||||
|
||||
async function handleClick() {
|
||||
async function handleClick(e: MouseEvent) {
|
||||
// TODO: Remove this debugging option to switch from web to desktop window buttons
|
||||
if (e.ctrlKey && e.shiftKey && e.altKey) {
|
||||
editor.handle.appWindowMinimize();
|
||||
editor.handle.appWindowMinimize();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
|
||||
else fullscreen.enterFullscreen();
|
||||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-buttons-web" on:click={() => handleClick()} tooltip={($fullscreen.windowFullscreen ? "Exit" : "Enter") + " Fullscreen (F11)"}>
|
||||
<LayoutRow class="window-buttons-web" on:click={handleClick} tooltip={$fullscreen.windowFullscreen ? "Exit Fullscreen (F11)" : "Enter Fullscreen (F11)"}>
|
||||
{#if requestFullscreenHotkeys}
|
||||
<TextLabel italic={true}>Go fullscreen to access all hotkeys</TextLabel>
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
export let maximized = false;
|
||||
export let maximized;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-button windows minimize" tooltip="Minimize">
|
||||
<LayoutRow class="window-button windows" tooltip="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
|
||||
<IconLabel icon={"WindowButtonWinMinimize"} />
|
||||
</LayoutRow>
|
||||
{#if !maximized}
|
||||
<LayoutRow class="window-button windows maximize" tooltip="Maximize">
|
||||
<IconLabel icon={"WindowButtonWinMaximize"} />
|
||||
</LayoutRow>
|
||||
{:else}
|
||||
<LayoutRow class="window-button windows restore-down" tooltip="Restore Down">
|
||||
<IconLabel icon={"WindowButtonWinRestoreDown"} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
<LayoutRow class="window-button windows close" tooltip="Close">
|
||||
<LayoutRow class="window-button windows" tooltip={maximized ? "Restore Down" : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
|
||||
<IconLabel icon={maximized ? "WindowButtonWinRestoreDown" : "WindowButtonWinMaximize"} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="window-button windows" tooltip="Close" on:click={() => editor.handle.appWindowClose()}>
|
||||
<IconLabel icon={"WindowButtonWinClose"} />
|
||||
</LayoutRow>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
&:last-of-type:hover {
|
||||
background: #e81123;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
export let clickAction: ((index: number) => void) | undefined = undefined;
|
||||
export let closeAction: ((index: number) => void) | undefined = undefined;
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
|
||||
let tabElements: (LayoutRow | undefined)[] = [];
|
||||
|
||||
function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
|
||||
@@ -90,7 +97,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<LayoutCol class="panel" on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)}>
|
||||
<LayoutCol on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}>
|
||||
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
|
||||
<LayoutRow class="tab-group" scrollableX={true}>
|
||||
{#each tabLabels as tabLabel, tabIndex}
|
||||
@@ -194,6 +201,7 @@
|
||||
.tab-bar {
|
||||
height: 28px;
|
||||
min-height: auto;
|
||||
background: var(--color-1-nearblack); // Needed for the viewport hole punch on desktop
|
||||
|
||||
&.min-widths .tab-group .tab {
|
||||
min-width: 120px;
|
||||
@@ -336,5 +344,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch &.document-panel,
|
||||
.viewport-hole-punch &.document-panel .panel-body:not(:has(.empty-panel)) {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["content"] }} data-subdivision-name="content">
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["document"] }} data-subdivision-name="document">
|
||||
<Panel
|
||||
class="document-panel"
|
||||
panelType={$portfolio.documents.length > 0 ? "Document" : undefined}
|
||||
tabCloseButtons={true}
|
||||
tabMinWidths={true}
|
||||
@@ -176,8 +177,9 @@
|
||||
flex: 1 1 100%;
|
||||
|
||||
.workspace-grid-subdivision {
|
||||
min-height: 28px;
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 28px;
|
||||
|
||||
&.folded {
|
||||
flex-grow: 0;
|
||||
@@ -196,5 +198,15 @@
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch & .workspace-grid-subdivision:has(.panel.document-panel)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
|
||||
import { type JsMessageType } from "@graphite/messages";
|
||||
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
import init, { setRandomSeed, wasmMemory, EditorHandle, receiveNativeMessage } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
export type Editor = {
|
||||
raw: WebAssembly.Memory;
|
||||
@@ -27,6 +27,8 @@ export async function initWasm() {
|
||||
wasmImport = await wasmMemory();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).imageCanvases = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).receiveNativeMessage = receiveNativeMessage;
|
||||
|
||||
// Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers
|
||||
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
|
||||
@@ -169,7 +169,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
potentiallyRestoreCanvasFocus(e);
|
||||
|
||||
const { target } = e;
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
|
||||
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
|
||||
const inContextMenu = target instanceof Element && target.closest("[data-context-menu]");
|
||||
const inTextInput = target === textToolInteractiveInputElement;
|
||||
@@ -219,7 +219,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
// Allow only events within the viewport or node graph boundaries
|
||||
const { target } = e;
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
|
||||
if (!(isTargetingCanvas instanceof Element)) return;
|
||||
|
||||
// Allow only repeated increments of double-clicks (not 1, 3, 5, etc.)
|
||||
@@ -256,7 +256,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
function onWheelScroll(e: WheelEvent) {
|
||||
const { target } = e;
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-node-graph]");
|
||||
const isTargetingCanvas = target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
|
||||
|
||||
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
|
||||
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation
|
||||
@@ -502,7 +502,9 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
function potentiallyRestoreCanvasFocus(e: Event) {
|
||||
const { target } = e;
|
||||
const newInCanvasArea = (target instanceof Element && target.closest("[data-viewport], [data-graph]")) instanceof Element && !targetIsTextField(window.document.activeElement || undefined);
|
||||
const newInCanvasArea =
|
||||
(target instanceof Element && target.closest("[data-viewport], [data-viewport-container], [data-graph]")) instanceof Element &&
|
||||
!targetIsTextField(window.document.activeElement || undefined);
|
||||
if (!canvasFocused && newInCanvasArea) {
|
||||
canvasFocused = true;
|
||||
app?.focus();
|
||||
|
||||
+25
-10
@@ -349,6 +349,21 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage {
|
||||
documentId!: string;
|
||||
}
|
||||
|
||||
export type AppWindowPlatform = "Web" | "Windows" | "Mac" | "Linux";
|
||||
|
||||
export class UpdatePlatform extends JsMessage {
|
||||
@Transform(({ value }: { value: AppWindowPlatform }) => value)
|
||||
readonly platform!: AppWindowPlatform;
|
||||
}
|
||||
|
||||
export class UpdateMaximized extends JsMessage {
|
||||
readonly maximized!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateViewportHolePunch extends JsMessage {
|
||||
readonly active!: boolean;
|
||||
}
|
||||
|
||||
export class UpdateInputHints extends JsMessage {
|
||||
@Type(() => HintInfo)
|
||||
readonly hintData!: HintData;
|
||||
@@ -501,7 +516,7 @@ export class Color {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const context = canvas.getContext("2d");
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) return undefined;
|
||||
|
||||
context.clearRect(0, 0, 1, 1);
|
||||
@@ -776,8 +791,6 @@ export class TriggerImport extends JsMessage {}
|
||||
|
||||
export class TriggerPaste extends JsMessage {}
|
||||
|
||||
export class TriggerDelayedZoomCanvasToFitAll extends JsMessage {}
|
||||
|
||||
export class TriggerDownloadImage extends JsMessage {
|
||||
readonly svg!: string;
|
||||
|
||||
@@ -1634,7 +1647,6 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
DisplayRemoveEditableTextbox,
|
||||
SendUIMetadata,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
TriggerDelayedZoomCanvasToFitAll,
|
||||
TriggerDownloadImage,
|
||||
TriggerDownloadTextFile,
|
||||
TriggerFetchAndOpenDocument,
|
||||
@@ -1670,29 +1682,32 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateGraphFadeArtwork,
|
||||
UpdateGraphViewOverlay,
|
||||
UpdateSpreadsheetState,
|
||||
UpdateImportReorderIndex,
|
||||
UpdateImportsExports,
|
||||
UpdateInputHints,
|
||||
UpdateInSelectedNetwork,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
UpdateLayerWidths,
|
||||
UpdateMaximized,
|
||||
UpdateMenuBarLayout,
|
||||
UpdateMouseCursor,
|
||||
UpdateNodeGraphNodes,
|
||||
UpdateVisibleNodes,
|
||||
UpdateNodeGraphWires,
|
||||
UpdateNodeGraphTransform,
|
||||
UpdateNodeGraphControlBarLayout,
|
||||
UpdateNodeGraphNodes,
|
||||
UpdateNodeGraphSelection,
|
||||
UpdateNodeGraphTransform,
|
||||
UpdateNodeGraphWires,
|
||||
UpdateNodeThumbnail,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdatePlatform,
|
||||
UpdatePropertyPanelSectionsLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateSpreadsheetState,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateViewportHolePunch,
|
||||
UpdateVisibleNodes,
|
||||
UpdateWirePathInProgress,
|
||||
UpdateWorkingColorsLayout,
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { type AppWindowPlatform, UpdatePlatform, UpdateMaximized, UpdateViewportHolePunch } from "@graphite/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createAppWindowState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
platform: "Web" as AppWindowPlatform,
|
||||
maximized: false,
|
||||
viewportHolePunch: false,
|
||||
});
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePlatform, (updatePlatform) => {
|
||||
update((state) => {
|
||||
state.platform = updatePlatform.platform;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMaximized, (maximized) => {
|
||||
update((state) => {
|
||||
state.maximized = maximized.maximized;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateViewportHolePunch, (viewportHolePunch) => {
|
||||
update((state) => {
|
||||
state.viewportHolePunch = viewportHolePunch.active;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
};
|
||||
}
|
||||
export type AppWindowState = ReturnType<typeof createAppWindowState>;
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
UpdateWorkingColorsLayout,
|
||||
UpdateNodeGraphControlBarLayout,
|
||||
UpdateGraphViewOverlay,
|
||||
TriggerDelayedZoomCanvasToFitAll,
|
||||
UpdateGraphFadeArtwork,
|
||||
} from "@graphite/messages";
|
||||
|
||||
@@ -94,12 +93,6 @@ export function createDocumentState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerDelayedZoomCanvasToFitAll, () => {
|
||||
// TODO: This is horribly hacky
|
||||
[0, 1, 10, 50, 100, 200, 300, 400, 500].forEach((delay) => {
|
||||
setTimeout(() => editor.handle.zoomCanvasToFitAll(), delay);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
@@ -103,7 +103,6 @@ export function createPortfolioState(editor: Editor) {
|
||||
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
|
||||
}
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => {
|
||||
update((state) => {
|
||||
state.spreadsheetOpen = updateSpreadsheetState.open;
|
||||
@@ -111,7 +110,6 @@ export function createPortfolioState(editor: Editor) {
|
||||
return state;
|
||||
});
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function textInputCleanup(text: string): string {
|
||||
// <https://github.com/WICG/keyboard-map/issues/26>
|
||||
// In the desktop version of VS Code, this is achieved with this Electron plugin:
|
||||
// <https://github.com/Microsoft/node-native-keymap>
|
||||
// We may be able to port that (it's a relatively small codebase) to Rust for use with Tauri.
|
||||
// We may be able to port that (it's a relatively small codebase) to Rust for use with our desktop application.
|
||||
// But on the web, just like VS Code, we're limited by the shortcomings of the spec.
|
||||
// A collection of further insights:
|
||||
// <https://docs.google.com/document/d/1p17IBbYGsZivLIMhKZOaCJFAJFokbPfKrkB37fOPXSM/edit>
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function imageToCanvasContext(imageData: ImageBitmapSource): Promis
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("Could not create canvas context");
|
||||
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ license = "Apache-2.0"
|
||||
[features]
|
||||
default = ["gpu"]
|
||||
gpu = ["editor/gpu"]
|
||||
tauri = ["editor/tauri"]
|
||||
native = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -38,6 +38,7 @@ wasm-bindgen-futures = { workspace = true }
|
||||
math-parser = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
wasm-opt = false
|
||||
|
||||
@@ -154,6 +154,7 @@ impl EditorHandle {
|
||||
}
|
||||
|
||||
// Sends a message to the dispatcher in the Editor Backend
|
||||
#[cfg(not(feature = "native"))]
|
||||
fn dispatch<T: Into<Message>>(&self, message: T) {
|
||||
// Process no further messages after a crash to avoid spamming the console
|
||||
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
@@ -169,6 +170,16 @@ impl EditorHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
fn dispatch<T: Into<Message>>(&self, message: T) {
|
||||
let message: Message = message.into();
|
||||
let Ok(serialized_message) = ron::to_string(&message) else {
|
||||
log::error!("Failed to serialize message");
|
||||
return;
|
||||
};
|
||||
crate::native_communcation::send_message_to_cef(serialized_message)
|
||||
}
|
||||
|
||||
// Sends a FrontendMessage to JavaScript
|
||||
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
|
||||
if let FrontendMessage::UpdateImageData { ref image_data } = message {
|
||||
@@ -228,22 +239,15 @@ impl EditorHandle {
|
||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
||||
|
||||
if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
editor_and_handle(|editor, handle| {
|
||||
for message in editor.handle_message(InputPreprocessorMessage::CurrentTime {
|
||||
editor_and_handle(|_, handle| {
|
||||
handle.dispatch(InputPreprocessorMessage::CurrentTime {
|
||||
timestamp: js_sys::Date::now() as u64,
|
||||
}) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
|
||||
for message in editor.handle_message(AnimationMessage::IncrementFrameCounter) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
});
|
||||
handle.dispatch(AnimationMessage::IncrementFrameCounter);
|
||||
|
||||
// Used by auto-panning, but this could possibly be refactored in the future, see:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/2562#discussion_r2041102786>
|
||||
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
handle.dispatch(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,6 +274,27 @@ impl EditorHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimizes the application window to the taskbar or dock
|
||||
#[wasm_bindgen(js_name = appWindowMinimize)]
|
||||
pub fn app_window_minimize(&self) {
|
||||
let message = AppWindowMessage::AppWindowMinimize;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Toggles minimizing or restoring down the application window
|
||||
#[wasm_bindgen(js_name = appWindowMaximize)]
|
||||
pub fn app_window_maximize(&self) {
|
||||
let message = AppWindowMessage::AppWindowMaximize;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Closes the application window
|
||||
#[wasm_bindgen(js_name = appWindowClose)]
|
||||
pub fn app_window_close(&self) {
|
||||
let message = AppWindowMessage::AppWindowClose;
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Displays a dialog with an error message
|
||||
#[wasm_bindgen(js_name = errorDialog)]
|
||||
pub fn error_dialog(&self, title: String, description: String) {
|
||||
@@ -889,7 +914,7 @@ fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) ->
|
||||
}
|
||||
|
||||
/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments.
|
||||
pub(crate) fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
|
||||
pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) {
|
||||
EDITOR_HANDLE.with(|editor_handle| {
|
||||
editor(|editor| {
|
||||
let mut guard = editor_handle.try_lock();
|
||||
@@ -910,7 +935,7 @@ async fn poll_node_graph_evaluation() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !editor::node_graph_executor::run_node_graph().await {
|
||||
if !editor::node_graph_executor::run_node_graph().await.0 {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -943,9 +968,7 @@ fn auto_save_all_documents() {
|
||||
return;
|
||||
}
|
||||
|
||||
editor_and_handle(|editor, handle| {
|
||||
for message in editor.handle_message(PortfolioMessage::AutoSaveAllDocuments) {
|
||||
handle.send_frontend_message_to_js(message);
|
||||
}
|
||||
editor_and_handle(|_, handle| {
|
||||
handle.dispatch(PortfolioMessage::AutoSaveAllDocuments);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ extern crate log;
|
||||
|
||||
pub mod editor_api;
|
||||
pub mod helpers;
|
||||
pub mod native_communcation;
|
||||
|
||||
use editor::messages::prelude::*;
|
||||
use std::panic;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use editor::{application::Editor, messages::prelude::FrontendMessage};
|
||||
use js_sys::{ArrayBuffer, Uint8Array};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::editor_api::{self, EditorHandle};
|
||||
|
||||
#[wasm_bindgen(js_name = "receiveNativeMessage")]
|
||||
pub fn receive_native_message(buffer: ArrayBuffer) {
|
||||
let buffer = Uint8Array::new(buffer.as_ref()).to_vec();
|
||||
match ron::from_str::<Vec<FrontendMessage>>(str::from_utf8(buffer.as_slice()).unwrap()) {
|
||||
Ok(messages) => {
|
||||
let callback = move |_: &mut Editor, handle: &mut EditorHandle| {
|
||||
for message in messages {
|
||||
handle.send_frontend_message_to_js_rust_proxy(message);
|
||||
}
|
||||
};
|
||||
editor_api::editor_and_handle(callback);
|
||||
}
|
||||
Err(e) => log::error!("Failed to deserialize frontend messages: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message_to_cef(message: String) {
|
||||
let global = js_sys::global();
|
||||
|
||||
// Get the function by name
|
||||
let func = js_sys::Reflect::get(&global, &JsValue::from_str("sendNativeMessage")).expect("Function not found");
|
||||
|
||||
let func = func.dyn_into::<js_sys::Function>().expect("Not a function");
|
||||
let array = Uint8Array::from(message.as_bytes());
|
||||
let buffer = array.buffer();
|
||||
|
||||
// Call it with argument
|
||||
func.call1(&JsValue::NULL, &JsValue::from(buffer)).expect("Function call failed");
|
||||
}
|
||||
@@ -59,13 +59,6 @@ in
|
||||
samply
|
||||
cargo-flamegraph
|
||||
|
||||
# For Tauri
|
||||
openssl
|
||||
glib
|
||||
gtk3
|
||||
libsoup
|
||||
webkitgtk
|
||||
|
||||
# For Rawkit tests
|
||||
libraw
|
||||
|
||||
|
||||
@@ -57,6 +57,15 @@ pub struct ImageTexture {
|
||||
pub texture: (),
|
||||
}
|
||||
|
||||
impl<'a> serde::Deserialize<'a> for ImageTexture {
|
||||
fn deserialize<D>(_: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
unimplemented!("attempted to serialize a texture")
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
|
||||
@@ -2,9 +2,9 @@ use crate::Ctx;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
|
||||
/// Obtains the X or Y component of a coordinate point.
|
||||
/// Obtains the X or Y component of a vec2.
|
||||
///
|
||||
/// The inverse of this node is "Coordinate Value", which can have either or both its X and Y exposed as graph inputs.
|
||||
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
||||
match axis {
|
||||
@@ -13,7 +13,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
|
||||
}
|
||||
}
|
||||
|
||||
/// The X or Y component of a coordinate.
|
||||
/// The X or Y component of a vec2.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum XY {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use super::intersection::bezpath_intersections;
|
||||
use super::poisson_disk::poisson_disk_sample;
|
||||
use super::util::segment_tangent;
|
||||
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
|
||||
use glam::DVec2;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
|
||||
use glam::{DMat2, DVec2};
|
||||
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
|
||||
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
|
||||
// Divide the segment.
|
||||
@@ -53,7 +55,19 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
|
||||
Some((first_bezpath, second_bezpath))
|
||||
}
|
||||
|
||||
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
split_bezpath_at_segment(bezpath, segment_index, t)
|
||||
}
|
||||
|
||||
pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
||||
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
||||
}
|
||||
@@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool {
|
||||
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
|
||||
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
|
||||
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
|
||||
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
|
||||
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
|
||||
// Split the first subpath at its last intersection
|
||||
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
|
||||
if subpath_1_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = *subpath_1_intersections.last()?;
|
||||
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;
|
||||
|
||||
// Split the second subpath at its first intersection
|
||||
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
|
||||
if subpath_2_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = subpath_2_intersections[0];
|
||||
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;
|
||||
|
||||
Some((clipped_subpath1, clipped_subpath2))
|
||||
}
|
||||
|
||||
/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
|
||||
///
|
||||
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
|
||||
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
|
||||
/// When the limit is exceeded, no [`PathEl`] will be returned.
|
||||
/// This value should be greater than 0. If not, the default of 4 will be used.
|
||||
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
|
||||
let miter_limit = match miter_limit {
|
||||
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
|
||||
_ => 4.,
|
||||
};
|
||||
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
|
||||
let in_segment = bezpath1.segments().last()?;
|
||||
let out_segment = bezpath2.segments().next()?;
|
||||
|
||||
let in_tangent = segment_tangent(in_segment, 1.);
|
||||
let out_tangent = segment_tangent(out_segment, 0.);
|
||||
|
||||
if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
|
||||
// Avoid panic from normalizing zero vectors
|
||||
// TODO: Besides returning None, is there a more appropriate way to handle this?
|
||||
return None;
|
||||
}
|
||||
|
||||
let angle = (in_tangent * -1.).angle_to(out_tangent).abs();
|
||||
|
||||
if angle.to_degrees() < miter_limit {
|
||||
return None;
|
||||
}
|
||||
|
||||
let p1 = in_segment.end();
|
||||
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
|
||||
let line1 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
let p1 = out_segment.start();
|
||||
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
|
||||
let line2 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
|
||||
// Otherwise, we return the element to create the join.
|
||||
let intersection = line1.crossing_point(line2)?;
|
||||
|
||||
Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
|
||||
}
|
||||
|
||||
/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
|
||||
/// By default, the angle is assumed to be 180 degrees.
|
||||
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
|
||||
let center_to_arc_point = arc_point - center;
|
||||
|
||||
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
|
||||
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
|
||||
|
||||
let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(arc_point);
|
||||
|
||||
let first_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(right);
|
||||
|
||||
let second_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
[first_half, second_half]
|
||||
}
|
||||
|
||||
/// Returns two [`PathEl`] to create a round join with the provided center.
|
||||
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
|
||||
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
let center_to_right = right - center;
|
||||
let center_to_left = left - center;
|
||||
|
||||
let in_segment = bezpath1.segments().last();
|
||||
let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.));
|
||||
|
||||
let mut angle = center_to_right.angle_to(center_to_left) / 2.;
|
||||
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
|
||||
if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
|
||||
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
|
||||
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
}
|
||||
|
||||
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
|
||||
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
#[cfg(test)]
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
@@ -0,0 +1,365 @@
|
||||
use super::contants::MIN_SEPARATION_VALUE;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
|
||||
|
||||
/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to
|
||||
/// that segment where the intersection occurred.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference that two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
pub fn bezpath_and_segment_intersections(bezpath: &BezPath, segment: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
bezpath
|
||||
.segments()
|
||||
.enumerate()
|
||||
.flat_map(|(index, this_segment)| {
|
||||
filtered_segment_intersections(this_segment, segment, accuracy, minimum_separation)
|
||||
.into_iter()
|
||||
.map(|t| (index, t))
|
||||
.collect::<Vec<(usize, f64)>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the bezpath has with another given bezpath and returns a list of parametric `t`-values.
|
||||
pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = bezpath2
|
||||
.segments()
|
||||
.flat_map(|bezier| bezpath_and_segment_intersections(bezpath1, bezier, accuracy, minimum_separation))
|
||||
.collect();
|
||||
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the segment has with another given segment and returns a list of parametric `t`-values with given accuracy.
|
||||
pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY);
|
||||
|
||||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections);
|
||||
intersections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments
|
||||
/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) {
|
||||
let bbox1 = segment1.bounding_box();
|
||||
let bbox2 = segment2.bounding_box();
|
||||
|
||||
let mid_t1 = (min_t1 + max_t1) / 2.;
|
||||
let mid_t2 = (min_t2 + max_t2) / 2.;
|
||||
|
||||
// Check if the bounding boxes overlap
|
||||
if bbox1.overlaps(bbox2) {
|
||||
// If bounding boxes overlap and they are small enough, we have found an intersection
|
||||
if bbox1.width() < accuracy && bbox1.height() < accuracy && bbox2.width() < accuracy && bbox2.height() < accuracy {
|
||||
// Use the middle `t` value, append the corresponding `t` value
|
||||
intersections.push((mid_t1, mid_t2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Split curves in half
|
||||
let (seg11, seg12) = segment1.subdivide();
|
||||
let (seg21, seg22) = segment2.subdivide();
|
||||
|
||||
// Repeat checking the intersection with the combinations of the two halves of each curve
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier segment and the provided one
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference
|
||||
/// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The returned `t` values are with respect to the current bezier segment, not the provided parameter.
|
||||
/// If the provided segment is linear, then zero intersection points will be returned along colinear segments.
|
||||
///
|
||||
/// `accuracy` defines, for intersections where the provided bezier segment is non-linear, the maximum size of the bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order.
|
||||
pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
intersection_t_values.iter().map(|x| x.0).fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of pairs of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided
|
||||
/// one such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference between
|
||||
/// two adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The first value in pair is with respect to the current bezier and the second value in pair is with respect to the provided parameter.
|
||||
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
|
||||
///
|
||||
/// `error`, for intersections where the provided bezier is non-linear, defines the threshold for bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order
|
||||
pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap());
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty()
|
||||
&& (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
&& (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
{
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{bezpath_and_segment_intersections, filtered_segment_intersections};
|
||||
use crate::vector::algorithms::{
|
||||
contants::MAX_ABSOLUTE_DIFFERENCE,
|
||||
util::{compare_points, compare_vec_of_points, dvec2_compare},
|
||||
};
|
||||
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez};
|
||||
|
||||
#[test]
|
||||
fn test_intersect_line_segment_quadratic() {
|
||||
let p1 = Point::new(30., 50.);
|
||||
let p2 = Point::new(140., 30.);
|
||||
let p3 = Point::new(160., 170.);
|
||||
|
||||
// Intersection at edge of curve
|
||||
let bezier = PathSeg::Quad(QuadBez::new(p1, p2, p3));
|
||||
let line1 = PathSeg::Line(Line::new(Point::new(20., 50.), Point::new(40., 50.)));
|
||||
let intersections1 = filtered_segment_intersections(bezier, line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.eval(intersections1[0]), p1));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(30., 30.)));
|
||||
let intersections2 = filtered_segment_intersections(bezier, line2, None, None);
|
||||
assert!(compare_points(bezier.eval(intersections2[0]), Point::new(47.77355, 47.77354)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve_cubic_edge_case() {
|
||||
// M34 107 C40 40 120 120 102 29
|
||||
|
||||
let p1 = Point::new(34., 107.);
|
||||
let p2 = Point::new(40., 40.);
|
||||
let p3 = Point::new(120., 120.);
|
||||
let p4 = Point::new(102., 29.);
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4));
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
let intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
|
||||
assert_eq!(intersections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve() {
|
||||
let p0 = Point::new(30., 30.);
|
||||
let p1 = Point::new(60., 140.);
|
||||
let p2 = Point::new(150., 30.);
|
||||
let p3 = Point::new(160., 160.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3));
|
||||
|
||||
let p0 = Point::new(175., 140.);
|
||||
let p1 = Point::new(20., 20.);
|
||||
let p2 = Point::new(120., 20.);
|
||||
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2));
|
||||
|
||||
let intersections1 = filtered_segment_intersections(cubic_segment, quadratic_segment, None, None);
|
||||
let intersections2 = filtered_segment_intersections(quadratic_segment, cubic_segment, None, None);
|
||||
|
||||
let intersections1_points: Vec<Point> = intersections1.iter().map(|&t| cubic_segment.eval(t)).collect();
|
||||
let intersections2_points: Vec<Point> = intersections2.iter().map(|&t| quadratic_segment.eval(t)).rev().collect();
|
||||
|
||||
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_one() {
|
||||
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(43., 43.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, linear_segment, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, linear_segment, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_two() {
|
||||
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
|
||||
// M150 150 L 20 20
|
||||
|
||||
let cubic_start = Point::new(34., 107.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(102., 29.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(129., 171.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_three() {
|
||||
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(44., 44.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
pub mod bezpath_algorithms;
|
||||
mod contants;
|
||||
pub mod instance;
|
||||
pub mod intersection;
|
||||
pub mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
pub mod util;
|
||||
|
||||
@@ -1,173 +1,137 @@
|
||||
use crate::vector::PointId;
|
||||
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
|
||||
use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join};
|
||||
use crate::vector::misc::point_to_dvec2;
|
||||
use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg};
|
||||
|
||||
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
|
||||
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
|
||||
/// Accuracy of fitting offset curve to Bezier paths.
|
||||
const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7;
|
||||
|
||||
fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier {
|
||||
match seg {
|
||||
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
|
||||
kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y),
|
||||
kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates(
|
||||
cubic_bez.p0.x,
|
||||
cubic_bez.p0.y,
|
||||
cubic_bez.p1.x,
|
||||
cubic_bez.p1.y,
|
||||
cubic_bez.p2.x,
|
||||
cubic_bez.p2.y,
|
||||
cubic_bez.p3.x,
|
||||
cubic_bez.p3.y,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation to use only Kurbo API.
|
||||
/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
|
||||
pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: Option<f64>) -> BezPath {
|
||||
// An offset at a distance 0 from the curve is simply the same curve.
|
||||
// An offset of a single point is not defined.
|
||||
if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 {
|
||||
return subpath.clone();
|
||||
if distance == 0. || bezpath.get_seg(1).is_none() {
|
||||
return bezpath.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = subpath
|
||||
.iter()
|
||||
.filter(|bezier| !bezier.is_point())
|
||||
let mut bezpaths = bezpath
|
||||
.segments()
|
||||
.map(|bezier| bezier.to_cubic())
|
||||
.map(|cubic| {
|
||||
let Bezier { start, end, handles } = cubic;
|
||||
let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()};
|
||||
|
||||
let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y));
|
||||
.map(|cubic_bez| {
|
||||
let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY);
|
||||
let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY);
|
||||
|
||||
let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| {
|
||||
acc.push(segment_to_bezier(seg));
|
||||
acc
|
||||
});
|
||||
|
||||
Subpath::from_beziers(&beziers, false)
|
||||
kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY)
|
||||
})
|
||||
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<Subpath<PointId>>>();
|
||||
|
||||
let mut drop_common_point = vec![true; subpath.len()];
|
||||
.filter(|bezpath| bezpath.get_seg(1).is_some()) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<BezPath>>();
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
for i in 0..bezpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let subpath1 = &subpaths[i];
|
||||
let subpath2 = &subpaths[j];
|
||||
let bezpath1 = &bezpaths[i];
|
||||
let bezpath2 = &bezpaths[j];
|
||||
|
||||
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
|
||||
let first_segment = subpath2.get_segment(0).unwrap();
|
||||
let last_segment_end = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let first_segment_start = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
// If the anchors are approximately equal, there is no need to clip / join the segments
|
||||
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
|
||||
if last_segment_end.abs_diff_eq(first_segment_start, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the angle formed between two consecutive Subpaths
|
||||
let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
|
||||
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
|
||||
// the points as being on top of one another.
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
|
||||
subpaths[i] = clipped_subpath1;
|
||||
subpaths[j] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
|
||||
bezpaths[i] = clipped_subpath1;
|
||||
bezpaths[j] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified join type
|
||||
if apply_join {
|
||||
drop_common_point[j] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[i].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
Join::Bevel => {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[i].push(element[0]);
|
||||
bezpaths[i].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor);
|
||||
let last_index = subpaths[i].manipulator_groups().len() - 1;
|
||||
subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[i].manipulator_groups_mut().push(round_point);
|
||||
subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
let center = point_to_dvec2(bezpath.get_seg(i + 1).unwrap().end());
|
||||
let elements = round_line_join(&bezpaths[i], &bezpaths[j], center);
|
||||
bezpaths[i].push(elements[0]);
|
||||
bezpaths[i].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if subpath.closed {
|
||||
let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath);
|
||||
if is_bezpath_closed {
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = subpaths.len() - 1;
|
||||
subpaths[last_index] = clipped_subpath1;
|
||||
subpaths[0] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = bezpaths.len() - 1;
|
||||
bezpaths[last_index] = clipped_subpath1;
|
||||
bezpaths[0] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
|
||||
if apply_join {
|
||||
drop_common_point[0] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
Join::Bevel => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = miter_line_join(&bezpaths[last_subpath_index], &bezpaths[0], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[last_subpath_index].push(element[0]);
|
||||
bezpaths[last_subpath_index].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor);
|
||||
let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1;
|
||||
subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(round_point);
|
||||
subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let center = point_to_dvec2(bezpath.get_seg(1).unwrap().start());
|
||||
let elements = round_line_join(&bezpaths[last_subpath_index], &bezpaths[0], center);
|
||||
bezpaths[last_subpath_index].push(elements[0]);
|
||||
bezpaths[last_subpath_index].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec();
|
||||
for i in 1..subpaths.len() {
|
||||
if drop_common_point[i] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec();
|
||||
manipulators_copy[0].in_handle = last_group.in_handle;
|
||||
|
||||
manipulator_groups.append(&mut manipulators_copy);
|
||||
} else {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec());
|
||||
// Merge the bezpaths and its segments. Drop points which overlap with one another.
|
||||
let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::<Vec<PathSeg>>()).collect::<Vec<PathSeg>>();
|
||||
let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| {
|
||||
if acc.elements().is_empty() {
|
||||
acc.move_to(segment.start());
|
||||
}
|
||||
}
|
||||
if subpath.closed && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
acc.push(segment.as_path_el());
|
||||
acc
|
||||
});
|
||||
|
||||
if is_bezpath_closed {
|
||||
offset_bezpath.close_path();
|
||||
}
|
||||
|
||||
Subpath::new(manipulator_groups, subpath.closed)
|
||||
offset_bezpath
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use glam::DVec2;
|
||||
use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg};
|
||||
|
||||
pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 {
|
||||
// NOTE: .deriv() method gives inaccurate result when it is 1.
|
||||
let t = if t == 1. { 1. - f64::EPSILON } else { t };
|
||||
|
||||
let tangent = match segment {
|
||||
PathSeg::Line(line) => line.deriv().eval(t),
|
||||
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
|
||||
};
|
||||
|
||||
DVec2::new(tangent.x, tangent.y)
|
||||
}
|
||||
|
||||
// Compare two f64s with some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
|
||||
(f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE
|
||||
}
|
||||
|
||||
/// Compare points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_points(p1: kurbo::Point, p2: kurbo::Point) -> bool {
|
||||
let (p1, p2) = (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2));
|
||||
p1.abs_diff_eq(p2, super::contants::MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
||||
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_vec_of_points(a: Vec<kurbo::Point>, b: Vec<kurbo::Point>, max_absolute_difference: f64) -> bool {
|
||||
a.len() == b.len()
|
||||
&& a.into_iter()
|
||||
.zip(b)
|
||||
.map(|(p1, p2)| (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2)))
|
||||
.all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
|
||||
}
|
||||
|
||||
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
|
||||
#[cfg(test)]
|
||||
pub fn dvec2_compare(a: kurbo::Point, b: kurbo::Point, max_abs_diff: f64) -> glam::BVec2 {
|
||||
glam::BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::offset_subpath::offset_subpath;
|
||||
use super::algorithms::bezpath_algorithms::{self, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::offset_subpath::offset_bezpath;
|
||||
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||
use super::misc::{CentroidType, point_to_dvec2};
|
||||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
@@ -17,8 +17,7 @@ use crate::vector::misc::{handles_to_segment, segment_to_handles};
|
||||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use crate::vector::{FillId, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
|
||||
use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath};
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use core::f64::consts::PI;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use glam::{DAffine2, DVec2};
|
||||
@@ -931,13 +930,13 @@ async fn dimensions(_: impl Ctx, vector_data: VectorDataTable) -> DVec2 {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Converts a coordinate value into a vector anchor point.
|
||||
/// Converts a vec2 value into a vector path composed of a single anchor point.
|
||||
///
|
||||
/// This is useful in conjunction with nodes that repeat it, followed by the "Points to Polyline" node to string together a path of the points.
|
||||
#[node_macro::node(category("Vector"), name("Coordinate to Point"), path(graphene_core::vector))]
|
||||
async fn position_to_point(_: impl Ctx, coordinate: DVec2) -> VectorDataTable {
|
||||
#[node_macro::node(category("Vector"), name("Vec2 to Point"), path(graphene_core::vector))]
|
||||
async fn vec2_to_point(_: impl Ctx, vec2: DVec2) -> VectorDataTable {
|
||||
let mut point_domain = PointDomain::new();
|
||||
point_domain.push(PointId::generate(), coordinate);
|
||||
point_domain.push(PointId::generate(), vec2);
|
||||
|
||||
VectorDataTable::new_instance(Instance {
|
||||
instance: VectorData { point_domain, ..Default::default() },
|
||||
@@ -979,10 +978,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
||||
vector_data
|
||||
.instance_iter()
|
||||
.map(|mut vector_data_instance| {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data_transform = Affine::new(vector_data_instance.transform.to_cols_array());
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let subpaths = vector_data.stroke_bezier_paths();
|
||||
let bezpaths = vector_data.stroke_bezpath_iter();
|
||||
let mut result = VectorData {
|
||||
style: vector_data.style.clone(),
|
||||
..Default::default()
|
||||
@@ -990,24 +989,25 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Perform operation on all subpaths in this shape.
|
||||
for mut subpath in subpaths {
|
||||
subpath.apply_transform(vector_data_transform);
|
||||
for mut bezpath in bezpaths {
|
||||
bezpath.apply_affine(vector_data_transform);
|
||||
|
||||
// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
|
||||
let mut subpath_out = offset_subpath(
|
||||
&subpath,
|
||||
let mut bezpath_out = offset_bezpath(
|
||||
&bezpath,
|
||||
-distance,
|
||||
match join {
|
||||
StrokeJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
StrokeJoin::Miter => kurbo::Join::Miter,
|
||||
StrokeJoin::Bevel => kurbo::Join::Bevel,
|
||||
StrokeJoin::Round => kurbo::Join::Round,
|
||||
},
|
||||
Some(miter_limit),
|
||||
);
|
||||
|
||||
subpath_out.apply_transform(vector_data_transform.inverse());
|
||||
bezpath_out.apply_affine(vector_data_transform.inverse());
|
||||
|
||||
// One closed subpath, open path.
|
||||
result.append_subpath(subpath_out, false);
|
||||
result.append_bezpath(bezpath_out);
|
||||
}
|
||||
|
||||
vector_data_instance.instance = result;
|
||||
@@ -1325,7 +1325,7 @@ async fn position_on_path(
|
||||
let t = if progress == bezpath_count { 1. } else { progress.fract() };
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None))
|
||||
point_to_dvec2(evaluate_bezpath(bezpath, t, euclidian, None))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user