From 34a8b9b6f19d9959d51863b3f15018bed3a1f88b Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 2 Aug 2025 16:27:24 +0200 Subject: [PATCH] Desktop: Render overlays with vello (#2965) * Render overlays with vello * Fix nix flake comments * Rendering refactor with better names and code location * Remove unnecessary overlay renders * Post rebase fix --- .nix/flake.nix | 3 +- Cargo.lock | 4 + desktop/Cargo.toml | 3 + desktop/src/app.rs | 17 +- desktop/src/render.rs | 313 +---------------- ...een_texture.wgsl => composite_shader.wgsl} | 34 +- desktop/src/render/frame_buffer_ref.rs | 53 +++ desktop/src/render/graphics_state.rs | 322 +++++++++++++++++ editor/Cargo.toml | 1 + .../src/messages/frontend/frontend_message.rs | 12 +- .../portfolio/document/overlays/mod.rs | 6 +- .../overlays/overlays_message_handler.rs | 24 +- .../document/overlays/utility_types_vello.rs | 331 +++++++++++++++--- node-graph/wgpu-executor/src/lib.rs | 6 +- 14 files changed, 728 insertions(+), 401 deletions(-) rename desktop/src/render/{fullscreen_texture.wgsl => composite_shader.wgsl} (61%) create mode 100644 desktop/src/render/frame_buffer_ref.rs create mode 100644 desktop/src/render/graphics_state.rs diff --git a/.nix/flake.nix b/.nix/flake.nix index 1371c4067..7b1b1da37 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -4,11 +4,12 @@ # # Development Environment: # - Provides all necessary tools for Rust/Wasm development +# - Includes dependencies for desktop app development # - Sets up profiling and debugging tools # - Configures mold as the default linker for faster builds # # Usage: -# - Development shell: `nix develop` +# - Development shell: `nix develop .nix` from the project root # - Run in dev shell with direnv: add `use flake` to .envrc { description = "Development environment and build configuration"; diff --git a/Cargo.lock b/Cargo.lock index 961b67c1a..7fee0af4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1836,16 +1836,19 @@ version = "0.1.0" dependencies = [ "bytemuck", "cef", + "derivative", "dirs", "futures", "glam", "graph-craft", + "graphene-std", "graphite-editor", "include_dir", "ron", "thiserror 2.0.12", "tracing", "tracing-subscriber", + "vello", "wgpu", "wgpu-executor", "winit", @@ -1880,6 +1883,7 @@ dependencies = [ "spin", "thiserror 2.0.12", "tokio", + "tracing", "usvg", "vello", "wasm-bindgen", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 97f6bca65..8b46d717e 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -19,6 +19,7 @@ graphite-editor = { path = "../editor", features = [ "ron", "vello", ] } +graphene-std = { workspace = true } graph-craft = { workspace = true } wgpu-executor = { workspace = true } @@ -34,3 +35,5 @@ dirs = { workspace = true } ron = { workspace = true} bytemuck = { workspace = true } glam = { workspace = true } +vello = { workspace = true } +derivative = { workspace = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index ae1fdc297..53876e12a 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -48,7 +48,15 @@ impl WinitApp { self.send_messages_to_editor(responses); } - fn send_messages_to_editor(&mut self, responses: Vec) { + fn send_messages_to_editor(&mut self, mut responses: Vec) { + for message in responses.extract_if(.., |m| matches!(m, FrontendMessage::RenderOverlays(_))) { + let FrontendMessage::RenderOverlays(overlay_context) = message else { unreachable!() }; + if let Some(graphics_state) = &mut self.graphics_state { + let scene = overlay_context.take_scene(); + graphics_state.set_overlays_scene(scene); + } + } + if responses.is_empty() { return; } @@ -110,8 +118,8 @@ impl ApplicationHandler for WinitApp { match event { CustomEvent::UiUpdate(texture) => { if let Some(graphics_state) = self.graphics_state.as_mut() { - graphics_state.bind_ui_texture(&texture); graphics_state.resize(texture.width(), texture.height()); + graphics_state.bind_ui_texture(texture); } if let Some(window) = &self.window { window.request_redraw(); @@ -125,7 +133,7 @@ impl ApplicationHandler for WinitApp { } } CustomEvent::MessageReceived { message } => { - if let Message::InputPreprocessor(ipp_message) = &message { + if let Message::InputPreprocessor(_) = &message { if let Some(window) = &self.window { window.request_redraw(); } @@ -144,13 +152,14 @@ impl ApplicationHandler for WinitApp { 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); + graphics_state.bind_viewport_texture(texture); } let mut responses = VecDeque::new(); let err = self.editor.poll_node_graph_evaluation(&mut responses); diff --git a/desktop/src/render.rs b/desktop/src/render.rs index 73f54e8d8..0fe3ad7d0 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -1,310 +1,5 @@ -use std::sync::Arc; +mod frame_buffer_ref; +pub(crate) use frame_buffer_ref::FrameBufferRef; -use bytemuck::{Pod, Zeroable}; -use thiserror::Error; -use winit::window::Window; - -pub(crate) struct FrameBufferRef<'a> { - buffer: &'a [u8], - width: usize, - height: usize, -} -impl<'a> FrameBufferRef<'a> { - pub(crate) fn new(buffer: &'a [u8], width: usize, height: usize) -> Result { - let fb = Self { buffer, width, height }; - fb.validate_size()?; - Ok(fb) - } - pub(crate) fn buffer(&self) -> &[u8] { - self.buffer - } - - pub(crate) fn width(&self) -> usize { - self.width - } - - pub(crate) fn height(&self) -> usize { - self.height - } - - fn validate_size(&self) -> Result<(), FrameBufferError> { - if self.buffer.len() != self.width * self.height * 4 { - Err(FrameBufferError::InvalidSize { - buffer_size: self.buffer.len(), - expected_size: self.width * self.height * 4, - width: self.width, - height: self.height, - }) - } else { - Ok(()) - } - } -} -impl<'a> std::fmt::Debug for FrameBufferRef<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FrameBuffer") - .field("width", &self.width) - .field("height", &self.height) - .field("len", &self.buffer.len()) - .finish() - } -} - -#[derive(Error, Debug)] -pub(crate) enum FrameBufferError { - #[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")] - InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize }, -} - -pub use wgpu_executor::Context as WgpuContext; - -#[derive(Debug)] -pub(crate) struct GraphicsState { - surface: wgpu::Surface<'static>, - context: WgpuContext, - config: wgpu::SurfaceConfiguration, - render_pipeline: wgpu::RenderPipeline, - sampler: wgpu::Sampler, - viewport_scale: [f32; 2], - viewport_offset: [f32; 2], - viewport_texture: Option, - ui_texture: Option, - bind_group: Option, -} - -impl GraphicsState { - pub(crate) fn new(window: Arc, context: WgpuContext) -> Self { - let size = window.inner_size(); - - let surface = context.instance.create_surface(window).unwrap(); - - let surface_caps = surface.get_capabilities(&context.adapter); - let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]); - - let config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, - width: size.width, - height: size.height, - present_mode: surface_caps.present_modes[0], - alpha_mode: surface_caps.alpha_modes[0], - view_formats: vec![], - desired_maximum_frame_latency: 2, - }; - - surface.configure(&context.device, &config); - - // Create shader module - let shader = context.device.create_shader_module(wgpu::include_wgsl!("render/fullscreen_texture.wgsl")); - - // Create sampler - let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - let texture_bind_group_layout = context.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::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, - }, - ], - label: Some("texture_bind_group_layout"), - }); - - 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: &[wgpu::PushConstantRange { - stages: wgpu::ShaderStages::FRAGMENT, - range: 0..size_of::() as u32, - }], - }); - - let render_pipeline = context.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: config.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }); - - Self { - surface, - context, - config, - render_pipeline, - sampler, - viewport_scale: [1.0, 1.0], - viewport_offset: [0.0, 0.0], - viewport_texture: None, - ui_texture: None, - bind_group: None, - } - } - - pub(crate) fn resize(&mut self, width: u32, height: u32) { - if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) { - self.config.width = width; - self.config.height = height; - self.surface.configure(&self.context.device, &self.config); - } - } - - 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); - } - - 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(&ui_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&viewport_texture_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - ], - label: Some("texture_bind_group"), - }) - } - - pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - let output = self.surface.get_current_texture()?; - let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Render Pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - - render_pass.set_pipeline(&self.render_pipeline); - 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 - } else { - tracing::warn!("No bind group available - showing clear color only"); - } - } - self.context.queue.submit(std::iter::once(encoder.finish())); - output.present(); - - Ok(()) - } -} - -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -struct Constants { - viewport_scale: [f32; 2], - viewport_offset: [f32; 2], -} +mod graphics_state; +pub(crate) use graphics_state::{GraphicsState, WgpuContext}; diff --git a/desktop/src/render/fullscreen_texture.wgsl b/desktop/src/render/composite_shader.wgsl similarity index 61% rename from desktop/src/render/fullscreen_texture.wgsl rename to desktop/src/render/composite_shader.wgsl index cdbe00bf5..b939ecbff 100644 --- a/desktop/src/render/fullscreen_texture.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -25,6 +25,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return out; } + struct Constants { viewport_scale: vec2, viewport_offset: vec2, @@ -33,19 +34,36 @@ struct Constants { var constants: Constants; @group(0) @binding(0) -var t_ui: texture_2d; -@group(0) @binding(1) var t_viewport: texture_2d; +@group(0) @binding(1) +var t_overlays: texture_2d; @group(0) @binding(2) +var t_ui: texture_2d; +@group(0) @binding(3) var s_diffuse: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let ui_color: vec4 = textureSample(t_ui, s_diffuse, in.tex_coords); - if (ui_color.a == 1.0) { - return ui_color; + let ui = textureSample(t_ui, s_diffuse, in.tex_coords); + if (ui.a >= 0.999) { + return ui; } - let viewport_tex_coords = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; - let viewport_color: vec4 = textureSample(t_viewport, s_diffuse, viewport_tex_coords); - return ui_color * ui_color.a + viewport_color * (1.0 - ui_color.a); + + let vp_cord = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; + + let ov = textureSample(t_overlays, s_diffuse, vp_cord); + let vp = textureSample(t_viewport, s_diffuse, vp_cord); + + if (ov.a < 0.001) { + return blend(ui, vp); + } + + let comp = blend(ov, vp); + return blend(ui, comp); +} + +fn blend(fg: vec4, bg: vec4) -> vec4 { + let a = fg.a + bg.a * (1.0 - fg.a); + let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a); + return vec4(rgb, a); } diff --git a/desktop/src/render/frame_buffer_ref.rs b/desktop/src/render/frame_buffer_ref.rs new file mode 100644 index 000000000..4135160b5 --- /dev/null +++ b/desktop/src/render/frame_buffer_ref.rs @@ -0,0 +1,53 @@ +use thiserror::Error; + +pub(crate) struct FrameBufferRef<'a> { + buffer: &'a [u8], + width: usize, + height: usize, +} +impl<'a> FrameBufferRef<'a> { + pub(crate) fn new(buffer: &'a [u8], width: usize, height: usize) -> Result { + let fb = Self { buffer, width, height }; + fb.validate_size()?; + Ok(fb) + } + pub(crate) fn buffer(&self) -> &[u8] { + self.buffer + } + + pub(crate) fn width(&self) -> usize { + self.width + } + + pub(crate) fn height(&self) -> usize { + self.height + } + + fn validate_size(&self) -> Result<(), FrameBufferError> { + if self.buffer.len() != self.width * self.height * 4 { + Err(FrameBufferError::InvalidSize { + buffer_size: self.buffer.len(), + expected_size: self.width * self.height * 4, + width: self.width, + height: self.height, + }) + } else { + Ok(()) + } + } +} +impl<'a> std::fmt::Debug for FrameBufferRef<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FrameBuffer") + .field("width", &self.width) + .field("height", &self.height) + .field("len", &self.buffer.len()) + .finish() + } +} + +#[derive(Error, Debug)] +pub(crate) enum FrameBufferError { + #[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")] + InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize }, +} diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs new file mode 100644 index 000000000..85a961531 --- /dev/null +++ b/desktop/src/render/graphics_state.rs @@ -0,0 +1,322 @@ +use graphene_std::Color; +use std::sync::Arc; +use wgpu_executor::WgpuExecutor; +use winit::window::Window; + +pub(crate) use wgpu_executor::Context as WgpuContext; + +#[derive(derivative::Derivative)] +#[derivative(Debug)] +pub(crate) struct GraphicsState { + surface: wgpu::Surface<'static>, + context: WgpuContext, + executor: WgpuExecutor, + config: wgpu::SurfaceConfiguration, + render_pipeline: wgpu::RenderPipeline, + transparent_texture: wgpu::Texture, + sampler: wgpu::Sampler, + viewport_scale: [f32; 2], + viewport_offset: [f32; 2], + viewport_texture: Option, + overlays_texture: Option, + ui_texture: Option, + bind_group: Option, + #[derivative(Debug = "ignore")] + overlays_scene: Option, +} + +impl GraphicsState { + pub(crate) fn new(window: Arc, context: WgpuContext) -> Self { + let size = window.inner_size(); + + let surface = context.instance.create_surface(window).unwrap(); + + let surface_caps = surface.get_capabilities(&context.adapter); + let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + surface.configure(&context.device, &config); + + let transparent_texture = context.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Transparent Texture"), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + // Create shader module + let shader = context.device.create_shader_module(wgpu::include_wgsl!("composite_shader.wgsl")); + + // Create sampler + let sampler = context.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let texture_bind_group_layout = context.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::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::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("texture_bind_group_layout"), + }); + + 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: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::FRAGMENT, + range: 0..size_of::() as u32, + }], + }); + + let render_pipeline = context.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + let wgpu_executor = WgpuExecutor::with_context(context.clone()).expect("Failed to create WgpuExecutor"); + + Self { + surface, + context, + executor: wgpu_executor, + config, + render_pipeline, + transparent_texture, + sampler, + viewport_scale: [1.0, 1.0], + viewport_offset: [0.0, 0.0], + viewport_texture: None, + overlays_texture: None, + ui_texture: None, + bind_group: None, + overlays_scene: None, + } + } + + pub(crate) fn resize(&mut self, width: u32, height: u32) { + if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) { + self.config.width = width; + self.config.height = height; + self.surface.configure(&self.context.device, &self.config); + } + } + + pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) { + self.viewport_texture = Some(viewport_texture); + self.update_bindgroup(); + } + + pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) { + self.overlays_texture = Some(overlays_texture); + self.update_bindgroup(); + } + + pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) { + self.ui_texture = Some(bind_ui_texture); + self.update_bindgroup(); + } + + 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; + } + + pub(crate) fn set_overlays_scene(&mut self, scene: vello::Scene) { + self.overlays_scene = Some(scene); + } + + fn render_overlays(&mut self, scene: vello::Scene) { + let Some(viewport_texture) = self.viewport_texture.as_ref() else { + tracing::warn!("No viewport texture bound, cannot render overlays"); + return; + }; + let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height()); + let texture = futures::executor::block_on(self.executor.render_vello_scene_to_texture(&scene, size, &Default::default(), Color::TRANSPARENT)); + let Ok(texture) = texture else { + tracing::error!("Error rendering overlays"); + return; + }; + self.bind_overlays_texture(texture); + } + + pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + if let Some(scene) = self.overlays_scene.take() { + self.render_overlays(scene); + } + + let output = self.surface.get_current_texture()?; + let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + 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 + } else { + tracing::warn!("No bind group available - showing clear color only"); + } + } + self.context.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } + + fn update_bindgroup(&mut self) { + let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); + let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); + let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); + + let bind_group = 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(&viewport_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&overlays_texture_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&ui_texture_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + ], + label: Some("texture_bind_group"), + }); + + self.bind_group = Some(bind_group); + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct Constants { + viewport_scale: [f32; 2], + viewport_offset: [f32; 2], +} diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 6871b9831..ed020ef91 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -46,6 +46,7 @@ once_cell = { workspace = true } web-sys = { workspace = true } bytemuck = { workspace = true } vello = { workspace = true } +tracing = { workspace = true } # Required dependencies spin = "0.9.8" diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e3290db04..49d2ab50b 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -13,8 +13,12 @@ use graphene_std::raster::Image; use graphene_std::raster::color::Color; use graphene_std::text::{Font, TextAlign}; +#[cfg(not(target_arch = "wasm32"))] +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; + #[impl_message(Message, Frontend)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize, specta::Type)] +#[derivative(Debug, PartialEq)] pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog DisplayDialog { @@ -318,4 +322,10 @@ pub enum FrontendMessage { UpdateViewportHolePunch { active: bool, }, + #[cfg(not(target_arch = "wasm32"))] + RenderOverlays( + #[serde(skip, default = "OverlayContext::default")] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + OverlayContext, + ), } diff --git a/editor/src/messages/portfolio/document/overlays/mod.rs b/editor/src/messages/portfolio/document/overlays/mod.rs index ece75226d..48e5dd660 100644 --- a/editor/src/messages/portfolio/document/overlays/mod.rs +++ b/editor/src/messages/portfolio/document/overlays/mod.rs @@ -2,12 +2,8 @@ pub mod grid_overlays; mod overlays_message; mod overlays_message_handler; pub mod utility_functions; -#[cfg(target_arch = "wasm32")] +#[cfg_attr(not(target_arch = "wasm32"), path = "utility_types_vello.rs")] 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}; diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 083d1538b..63376b11c 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -73,34 +73,18 @@ impl MessageHandler> for OverlaysMes #[cfg(all(not(target_arch = "wasm32"), not(test)))] OverlaysMessage::Draw => { use super::utility_types::OverlayContext; - use vello::Scene; + let size = ipp.viewport_bounds.size(); - let size = ipp.viewport_bounds.size().as_uvec2(); - - let scene = Scene::new(); + let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings); 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)); + responses.add(provider(overlay_context.clone())); } } - - // TODO: Render the Vello scene to a texture and display it + responses.add(FrontendMessage::RenderOverlays(overlay_context)); } OverlaysMessage::AddProvider(message) => { self.overlay_providers.insert(message); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 77b338ca5..f7fabbb63 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -13,6 +13,7 @@ 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 std::sync::{Arc, Mutex, MutexGuard}; use vello::Scene; use vello::kurbo::{self, BezPath}; use vello::peniko; @@ -132,12 +133,12 @@ impl OverlaysVisibilitySettings { } } -#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(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, + internal: Arc>, 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. @@ -145,6 +146,22 @@ pub struct OverlayContext { pub visibility_settings: OverlaysVisibilitySettings, } +impl Clone for OverlayContext { + fn clone(&self) -> Self { + let internal = self.internal.lock().expect("Failed to lock internal overlay context"); + let size = internal.size; + let device_pixel_ratio = internal.device_pixel_ratio; + let visibility_settings = internal.visibility_settings; + drop(internal); // Explicitly release the lock before cloning the Arc> + Self { + internal: self.internal.clone(), + size, + device_pixel_ratio, + visibility_settings, + } + } +} + // Manual implementations since Scene doesn't implement PartialEq or Debug impl PartialEq for OverlayContext { fn eq(&self, other: &Self) -> bool { @@ -167,7 +184,7 @@ impl std::fmt::Debug for OverlayContext { impl Default for OverlayContext { fn default() -> Self { Self { - scene: Scene::new(), + internal: Mutex::new(OverlayContextInternal::default()).into(), size: DVec2::ZERO, device_pixel_ratio: 1.0, visibility_settings: OverlaysVisibilitySettings::default(), @@ -181,6 +198,235 @@ impl core::hash::Hash for OverlayContext { } impl OverlayContext { + pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self { + Self { + internal: Arc::new(Mutex::new(OverlayContextInternal::new(size, device_pixel_ratio, visibility_settings))), + size, + device_pixel_ratio, + visibility_settings, + } + } + + pub fn take_scene(self) -> Scene { + let mut internal = self.internal.lock().expect("Failed to lock internal overlay context"); + std::mem::take(&mut *internal).scene + } + + fn internal(&'_ self) -> MutexGuard<'_, OverlayContextInternal> { + self.internal.lock().expect("Failed to lock internal overlay context") + } + + pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) { + self.internal().quad(quad, stroke_color, color_fill); + } + + pub fn draw_triangle(&mut self, base: DVec2, direction: DVec2, size: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + self.internal().draw_triangle(base, direction, size, color_fill, color_stroke); + } + + pub fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.internal().dashed_quad(quad, 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.internal().polygon(polygon, stroke_color, color_fill); + } + + pub fn dashed_polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.internal().dashed_polygon(polygon, stroke_color, color_fill, dash_width, dash_gap_width, dash_offset); + } + + pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option) { + self.internal().line(start, end, color, thickness); + } + + #[allow(clippy::too_many_arguments)] + pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.internal().dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); + } + + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { + self.internal().hover_manipulator_handle(position, selected); + } + + pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { + self.internal().hover_manipulator_anchor(position, selected); + } + + pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.internal().manipulator_handle(position, selected, color); + } + + pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + self.internal().manipulator_anchor(position, selected, color); + } + + pub fn square(&mut self, position: DVec2, size: Option, color_fill: Option<&str>, color_stroke: Option<&str>) { + self.internal().square(position, size, color_fill, color_stroke); + } + + pub fn pixel(&mut self, position: DVec2, color: Option<&str>) { + self.internal().pixel(position, color); + } + + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + self.internal().circle(position, radius, color_fill, color_stroke); + } + + pub fn dashed_ellipse( + &mut self, + center: DVec2, + radius_x: f64, + radius_y: f64, + rotation: Option, + start_angle: Option, + end_angle: Option, + counterclockwise: Option, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + ) { + self.internal().dashed_ellipse( + center, + radius_x, + radius_y, + rotation, + start_angle, + end_angle, + counterclockwise, + color_fill, + color_stroke, + dash_width, + dash_gap_width, + dash_offset, + ); + } + + pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { + self.internal().draw_arc(center, radius, start_from, end_at); + } + + pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { + self.internal().draw_arc_gizmo_angle(pivot, bold_radius, arc_radius, offset_angle, angle); + } + + pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { + self.internal().draw_angle(pivot, radius, arc_radius, offset_angle, angle); + } + + pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) { + self.internal().draw_scale(start, scale, radius, text); + } + + pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option) { + self.internal().compass_rose(compass_center, angle, show_compass_with_hover_ring); + } + + pub fn pivot(&mut self, position: DVec2, angle: f64) { + self.internal().pivot(position, angle); + } + + pub fn dowel_pin(&mut self, position: DVec2, angle: f64, color: Option<&str>) { + self.internal().dowel_pin(position, angle, color); + } + + #[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.internal().arc_sweep_angle(offset_angle, angle, end_point_position, bold_radius, pivot, text, transform); + } + + /// 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) { + self.internal().outline_vector(vector_data, transform); + } + + /// 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) { + self.internal().outline_bezier(bezier, transform); + } + + /// 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) { + self.internal().outline_select_bezier(bezier, transform); + } + + pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + self.internal().outline_overlay_bezier(bezier, transform); + } + + /// 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>, transform: DAffine2, color: Option<&str>) { + self.internal().outline(target_types, transform, color); + } + + /// 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>>, transform: DAffine2, color: &str) { + self.internal().fill_path(subpaths, transform, color); + } + + /// 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>>, transform: DAffine2, color: &Color) { + self.internal().fill_path_pattern(subpaths, transform, color); + } + + pub fn get_width(&self, text: &str) -> f64 { + self.internal().get_width(text) + } + + pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { + self.internal().text(text, font_color, background_color, transform, padding, pivot); + } + + pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option) { + self.internal().translation_box(translation, quad, typed_string); + } +} + +pub enum Pivot { + Start, + Middle, + End, +} + +pub enum DrawHandles { + All, + SelectedAnchors(Vec), + FrontierHandles(HashMap>), + None, +} + +pub(super) struct OverlayContextInternal { + scene: Scene, + size: DVec2, + device_pixel_ratio: f64, + visibility_settings: OverlaysVisibilitySettings, +} + +impl Default for OverlayContextInternal { + fn default() -> Self { + Self { + scene: Scene::new(), + size: DVec2::ZERO, + device_pixel_ratio: 1.0, + visibility_settings: OverlaysVisibilitySettings::default(), + } + } +} + +impl OverlayContextInternal { + pub(super) fn new(size: DVec2, device_pixel_ratio: f64, visibility_settings: OverlaysVisibilitySettings) -> Self { + Self { + scene: Scene::new(), + size, + device_pixel_ratio, + visibility_settings, + } + } + 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); @@ -190,11 +436,11 @@ impl OverlayContext { peniko::Color::from_rgba8(r, g, b, a) } - pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) { + 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>) { + 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(); @@ -215,15 +461,15 @@ impl OverlayContext { 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, dash_gap_width: Option, dash_offset: Option) { + fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { 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>) { + 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, dash_gap_width: Option, dash_offset: Option) { + fn dashed_polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { if polygon.len() < 2 { return; } @@ -255,12 +501,12 @@ impl OverlayContext { 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) { + fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option) { 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, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { let transform = self.get_transform(); let start = start.round() - DVec2::splat(0.5); @@ -280,7 +526,7 @@ impl OverlayContext { 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>) { + 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); @@ -293,7 +539,7 @@ impl OverlayContext { .stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle); } - pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { + fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); @@ -311,13 +557,13 @@ impl OverlayContext { self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle); } - pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { + 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)); } - pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { + fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50)); let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); @@ -327,7 +573,7 @@ impl OverlayContext { kurbo::Affine::scale(self.device_pixel_ratio) } - pub fn square(&mut self, position: DVec2, size: Option, color_fill: Option<&str>, color_stroke: Option<&str>) { + fn square(&mut self, position: DVec2, size: Option, 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); @@ -343,7 +589,7 @@ impl OverlayContext { 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>) { + fn pixel(&mut self, position: DVec2, color: Option<&str>) { let size = 1.; let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE); @@ -356,7 +602,7 @@ impl OverlayContext { 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>) { + 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(); @@ -369,7 +615,7 @@ impl OverlayContext { self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle); } - pub fn dashed_ellipse( + fn dashed_ellipse( &mut self, _center: DVec2, _radius_x: f64, @@ -386,7 +632,7 @@ impl OverlayContext { ) { } - pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { + 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.; @@ -420,13 +666,13 @@ impl OverlayContext { 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) { + 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) { + 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); @@ -434,7 +680,7 @@ impl OverlayContext { 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) { + 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, '#'); @@ -452,7 +698,7 @@ impl OverlayContext { ) } - pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option) { + fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option) { 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.; @@ -508,7 +754,7 @@ impl OverlayContext { .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) { + fn pivot(&mut self, position: DVec2, angle: f64) { let uv = DVec2::from_angle(angle); let (x, y) = (position.round() - DVec2::splat(0.5)).into(); @@ -539,7 +785,7 @@ impl OverlayContext { 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>) { + 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); @@ -581,14 +827,14 @@ impl OverlayContext { } #[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) { + 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, None); 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) { + fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { let vello_transform = self.get_transform(); let mut path = BezPath::new(); @@ -604,7 +850,7 @@ impl OverlayContext { } /// 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) { + 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); @@ -613,7 +859,7 @@ impl OverlayContext { } /// 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) { + 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); @@ -621,7 +867,7 @@ impl OverlayContext { 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) { + 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); @@ -695,7 +941,7 @@ impl OverlayContext { } /// 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>, transform: DAffine2, color: Option<&str>) { + fn outline(&mut self, target_types: impl Iterator>, transform: DAffine2, color: Option<&str>) { let mut subpaths: Vec> = vec![]; for target_type in target_types { @@ -717,7 +963,7 @@ impl OverlayContext { /// 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>>, transform: DAffine2, color: &str) { + fn fill_path(&mut self, subpaths: impl Iterator>>, 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); @@ -725,7 +971,7 @@ impl OverlayContext { /// 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>>, transform: DAffine2, color: &Color) { + fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, 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); @@ -745,16 +991,16 @@ impl OverlayContext { ); } - pub fn get_width(&self, _text: &str) -> f64 { + 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]) { + 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) { + fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option) { if translation.x.abs() > 1e-3 { self.dashed_line(quad.top_left(), quad.top_right(), None, None, Some(2.), Some(2.), Some(0.5)); @@ -784,16 +1030,3 @@ impl OverlayContext { } } } - -pub enum Pivot { - Start, - Middle, - End, -} - -pub enum DrawHandles { - All, - SelectedAnchors(Vec), - FrontierHandles(HashMap>), - None, -} diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index 460948d1c..18e139d93 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -158,11 +158,9 @@ impl WgpuExecutor { }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let [r, g, b, _] = background.to_rgba8_srgb(); + let [r, g, b, a] = background.to_rgba8_srgb(); let render_params = RenderParams { - // We are using an explicit opaque color here to eliminate the alpha premultiplication step - // which would be required to support a transparent webgpu canvas - base_color: vello::peniko::Color::from_rgba8(r, g, b, 0xff), + base_color: vello::peniko::Color::from_rgba8(r, g, b, a), width: size.x, height: size.y, antialiasing_method: AaConfig::Msaa16,