Desktop: Render overlays with vello (#2965)
Some checks are pending
Editor: Dev & CI / build (push) Waiting to run
Editor: Dev & CI / cargo-deny (push) Waiting to run

* Render overlays with vello

* Fix nix flake comments

* Rendering refactor with better names and code location

* Remove unnecessary overlay renders

* Post rebase fix
This commit is contained in:
Timon 2025-08-02 16:27:24 +02:00 committed by GitHub
parent 037bcb6b26
commit 34a8b9b6f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 728 additions and 401 deletions

View file

@ -4,11 +4,12 @@
# #
# Development Environment: # Development Environment:
# - Provides all necessary tools for Rust/Wasm development # - Provides all necessary tools for Rust/Wasm development
# - Includes dependencies for desktop app development
# - Sets up profiling and debugging tools # - Sets up profiling and debugging tools
# - Configures mold as the default linker for faster builds # - Configures mold as the default linker for faster builds
# #
# Usage: # 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 # - Run in dev shell with direnv: add `use flake` to .envrc
{ {
description = "Development environment and build configuration"; description = "Development environment and build configuration";

4
Cargo.lock generated
View file

@ -1836,16 +1836,19 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"cef", "cef",
"derivative",
"dirs", "dirs",
"futures", "futures",
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-std",
"graphite-editor", "graphite-editor",
"include_dir", "include_dir",
"ron", "ron",
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"vello",
"wgpu", "wgpu",
"wgpu-executor", "wgpu-executor",
"winit", "winit",
@ -1880,6 +1883,7 @@ dependencies = [
"spin", "spin",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tracing",
"usvg", "usvg",
"vello", "vello",
"wasm-bindgen", "wasm-bindgen",

View file

@ -19,6 +19,7 @@ graphite-editor = { path = "../editor", features = [
"ron", "ron",
"vello", "vello",
] } ] }
graphene-std = { workspace = true }
graph-craft = { workspace = true } graph-craft = { workspace = true }
wgpu-executor = { workspace = true } wgpu-executor = { workspace = true }
@ -34,3 +35,5 @@ dirs = { workspace = true }
ron = { workspace = true} ron = { workspace = true}
bytemuck = { workspace = true } bytemuck = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
vello = { workspace = true }
derivative = { workspace = true }

View file

@ -48,7 +48,15 @@ impl WinitApp {
self.send_messages_to_editor(responses); self.send_messages_to_editor(responses);
} }
fn send_messages_to_editor(&mut self, responses: Vec<FrontendMessage>) { fn send_messages_to_editor(&mut self, mut responses: Vec<FrontendMessage>) {
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() { if responses.is_empty() {
return; return;
} }
@ -110,8 +118,8 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
match event { match event {
CustomEvent::UiUpdate(texture) => { CustomEvent::UiUpdate(texture) => {
if let Some(graphics_state) = self.graphics_state.as_mut() { 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.resize(texture.width(), texture.height());
graphics_state.bind_ui_texture(texture);
} }
if let Some(window) = &self.window { if let Some(window) = &self.window {
window.request_redraw(); window.request_redraw();
@ -125,7 +133,7 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
} }
} }
CustomEvent::MessageReceived { message } => { CustomEvent::MessageReceived { message } => {
if let Message::InputPreprocessor(ipp_message) = &message { if let Message::InputPreprocessor(_) = &message {
if let Some(window) = &self.window { if let Some(window) = &self.window {
window.request_redraw(); window.request_redraw();
} }
@ -144,13 +152,14 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
panic!("graphics state not intialized, viewport offset might be lost"); panic!("graphics state not intialized, viewport offset might be lost");
} }
} }
self.dispatch_message(message); self.dispatch_message(message);
} }
CustomEvent::NodeGraphRan { texture } => { CustomEvent::NodeGraphRan { texture } => {
if let Some(texture) = texture if let Some(texture) = texture
&& let Some(graphics_state) = &mut self.graphics_state && 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 mut responses = VecDeque::new();
let err = self.editor.poll_node_graph_evaluation(&mut responses); let err = self.editor.poll_node_graph_evaluation(&mut responses);

View file

@ -1,310 +1,5 @@
use std::sync::Arc; mod frame_buffer_ref;
pub(crate) use frame_buffer_ref::FrameBufferRef;
use bytemuck::{Pod, Zeroable}; mod graphics_state;
use thiserror::Error; pub(crate) use graphics_state::{GraphicsState, WgpuContext};
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<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
}
}
}
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<wgpu::Texture>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
}
impl GraphicsState {
pub(crate) fn new(window: Arc<Window>, 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::<Constants>() 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],
}

View file

@ -25,6 +25,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
return out; return out;
} }
struct Constants { struct Constants {
viewport_scale: vec2<f32>, viewport_scale: vec2<f32>,
viewport_offset: vec2<f32>, viewport_offset: vec2<f32>,
@ -33,19 +34,36 @@ struct Constants {
var<push_constant> constants: Constants; var<push_constant> constants: Constants;
@group(0) @binding(0) @group(0) @binding(0)
var t_ui: texture_2d<f32>;
@group(0) @binding(1)
var t_viewport: texture_2d<f32>; var t_viewport: texture_2d<f32>;
@group(0) @binding(1)
var t_overlays: texture_2d<f32>;
@group(0) @binding(2) @group(0) @binding(2)
var t_ui: texture_2d<f32>;
@group(0) @binding(3)
var s_diffuse: sampler; var s_diffuse: sampler;
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let ui_color: vec4<f32> = textureSample(t_ui, s_diffuse, in.tex_coords); let ui = textureSample(t_ui, s_diffuse, in.tex_coords);
if (ui_color.a == 1.0) { if (ui.a >= 0.999) {
return ui_color; return ui;
} }
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); let vp_cord = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale;
return ui_color * ui_color.a + viewport_color * (1.0 - ui_color.a);
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<f32>, bg: vec4<f32>) -> vec4<f32> {
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<f32>(rgb, a);
} }

View file

@ -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<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
}
}
}
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 },
}

View file

@ -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<wgpu::Texture>,
overlays_texture: Option<wgpu::Texture>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
#[derivative(Debug = "ignore")]
overlays_scene: Option<vello::Scene>,
}
impl GraphicsState {
pub(crate) fn new(window: Arc<Window>, 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::<Constants>() 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],
}

View file

@ -46,6 +46,7 @@ once_cell = { workspace = true }
web-sys = { workspace = true } web-sys = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }
vello = { workspace = true } vello = { workspace = true }
tracing = { workspace = true }
# Required dependencies # Required dependencies
spin = "0.9.8" spin = "0.9.8"

View file

@ -13,8 +13,12 @@ use graphene_std::raster::Image;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::text::{Font, TextAlign}; use graphene_std::text::{Font, TextAlign};
#[cfg(not(target_arch = "wasm32"))]
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
#[impl_message(Message, Frontend)] #[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 { pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog // Display prefix: make the frontend show something, like a dialog
DisplayDialog { DisplayDialog {
@ -318,4 +322,10 @@ pub enum FrontendMessage {
UpdateViewportHolePunch { UpdateViewportHolePunch {
active: bool, active: bool,
}, },
#[cfg(not(target_arch = "wasm32"))]
RenderOverlays(
#[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
OverlayContext,
),
} }

View file

@ -2,12 +2,8 @@ pub mod grid_overlays;
mod overlays_message; mod overlays_message;
mod overlays_message_handler; mod overlays_message_handler;
pub mod utility_functions; pub mod utility_functions;
#[cfg(target_arch = "wasm32")] #[cfg_attr(not(target_arch = "wasm32"), path = "utility_types_vello.rs")]
pub mod utility_types; 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)] #[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant}; pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};

View file

@ -73,34 +73,18 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
#[cfg(all(not(target_arch = "wasm32"), not(test)))] #[cfg(all(not(target_arch = "wasm32"), not(test)))]
OverlaysMessage::Draw => { OverlaysMessage::Draw => {
use super::utility_types::OverlayContext; use super::utility_types::OverlayContext;
use vello::Scene; let size = ipp.viewport_bounds.size();
let size = ipp.viewport_bounds.size().as_uvec2(); let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
let scene = Scene::new();
if visibility_settings.all() { 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())); responses.add(DocumentMessage::GridOverlays(overlay_context.clone()));
for provider in &self.overlay_providers { for provider in &self.overlay_providers {
let overlay_context = OverlayContext { responses.add(provider(overlay_context.clone()));
scene: Scene::new(),
size: size.as_dvec2(),
device_pixel_ratio,
visibility_settings,
};
responses.add(provider(overlay_context));
} }
} }
responses.add(FrontendMessage::RenderOverlays(overlay_context));
// TODO: Render the Vello scene to a texture and display it
} }
OverlaysMessage::AddProvider(message) => { OverlaysMessage::AddProvider(message) => {
self.overlay_providers.insert(message); self.overlay_providers.insert(message);

View file

@ -13,6 +13,7 @@ use graphene_std::math::quad::Quad;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::{PointId, SegmentId, VectorData}; use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};
use vello::Scene; use vello::Scene;
use vello::kurbo::{self, BezPath}; use vello::kurbo::{self, BezPath};
use vello::peniko; 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 { pub struct OverlayContext {
// Serde functionality isn't used but is required by the message system macros // Serde functionality isn't used but is required by the message system macros
#[serde(skip)] #[serde(skip)]
#[specta(skip)] #[specta(skip)]
pub scene: Scene, internal: Arc<Mutex<OverlayContextInternal>>,
pub size: DVec2, 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. // 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. // 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, 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<Mutex<_>>
Self {
internal: self.internal.clone(),
size,
device_pixel_ratio,
visibility_settings,
}
}
}
// Manual implementations since Scene doesn't implement PartialEq or Debug // Manual implementations since Scene doesn't implement PartialEq or Debug
impl PartialEq for OverlayContext { impl PartialEq for OverlayContext {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
@ -167,7 +184,7 @@ impl std::fmt::Debug for OverlayContext {
impl Default for OverlayContext { impl Default for OverlayContext {
fn default() -> Self { fn default() -> Self {
Self { Self {
scene: Scene::new(), internal: Mutex::new(OverlayContextInternal::default()).into(),
size: DVec2::ZERO, size: DVec2::ZERO,
device_pixel_ratio: 1.0, device_pixel_ratio: 1.0,
visibility_settings: OverlaysVisibilitySettings::default(), visibility_settings: OverlaysVisibilitySettings::default(),
@ -181,6 +198,235 @@ impl core::hash::Hash for OverlayContext {
} }
impl 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<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
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<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
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<f64>) {
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<f64>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
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<f64>, 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<f64>,
start_angle: Option<f64>,
end_angle: Option<f64>,
counterclockwise: Option<bool>,
color_fill: Option<&str>,
color_stroke: Option<&str>,
dash_width: Option<f64>,
dash_gap_width: Option<f64>,
dash_offset: Option<f64>,
) {
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<bool>) {
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<Item = impl Borrow<ClickTargetType>>, 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<Item = impl Borrow<Subpath<PointId>>>, 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<Item = impl Borrow<Subpath<PointId>>>, 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<String>) {
self.internal().translation_box(translation, quad, typed_string);
}
}
pub enum Pivot {
Start,
Middle,
End,
}
pub enum DrawHandles {
All,
SelectedAnchors(Vec<SegmentId>),
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
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 { fn parse_color(color: &str) -> peniko::Color {
let hex = color.trim_start_matches('#'); let hex = color.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); 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) 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); 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_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let normal = direction.perp(); 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); 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>) { 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); 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); 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>) { 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 { if polygon.len() < 2 {
return; return;
} }
@ -255,12 +501,12 @@ impl OverlayContext {
self.scene.stroke(&stroke, transform, Self::parse_color(stroke_color), None, &path); 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>) { fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>) {
self.dashed_line(start, end, color, thickness, None, None, None) self.dashed_line(start, end, color, thickness, None, None, None)
} }
#[allow(clippy::too_many_arguments)] #[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>) { 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 transform = self.get_transform();
let start = start.round() - DVec2::splat(0.5); 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); 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 transform = self.get_transform();
let position = position.round() - DVec2::splat(0.5); 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); .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 transform = self.get_transform();
let position = position.round() - DVec2::splat(0.5); 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); 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_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE }; let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
self.square(position, None, Some(color_fill), Some(color_stroke)); 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)); 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 }; let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
@ -327,7 +573,7 @@ impl OverlayContext {
kurbo::Affine::scale(self.device_pixel_ratio) 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>) { 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 size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); 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); 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 size = 1.;
let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE); 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); 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_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round(); 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); 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, &mut self,
_center: DVec2, _center: DVec2,
_radius_x: f64, _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 segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
let step = (end_at - start_from) / segments as f64; let step = (end_at - start_from) / segments as f64;
let half_step = step / 2.; 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); 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); let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
self.line(pivot, end_point1, None, None); self.line(pivot, end_point1, None, None);
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle); 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_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle); let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
self.line(pivot, end_point1, None, None); 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); 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 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(); 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, '#'); 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<bool>) { 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 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_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_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); .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 uv = DVec2::from_angle(angle);
let (x, y) = (position.round() - DVec2::splat(0.5)).into(); 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); 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 (x, y) = (position.round() - DVec2::splat(0.5)).into();
let color = color.unwrap_or(COLOR_OVERLAY_YELLOW_DULL); let color = color.unwrap_or(COLOR_OVERLAY_YELLOW_DULL);
@ -581,14 +827,14 @@ impl OverlayContext {
} }
#[allow(clippy::too_many_arguments)] #[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.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.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]); 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. /// 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 vello_transform = self.get_transform();
let mut path = BezPath::new(); 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. /// 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 vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); 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. /// 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 vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); 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); 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 vello_transform = self.get_transform();
let mut path = BezPath::new(); let mut path = BezPath::new();
self.bezier_to_path(bezier, transform, true, &mut path); 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. /// 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>) { 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![]; let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
for target_type in target_types { for target_type in target_types {
@ -717,7 +963,7 @@ impl OverlayContext {
/// Fills the area inside the path. Assumes `color` is in gamma space. /// Fills the area inside the path. Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed. /// 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) { fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
let path = self.push_path(subpaths, transform); let path = self.push_path(subpaths, transform);
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path); 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. /// 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. /// 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) { fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
// TODO: Implement pattern fill in Vello // TODO: Implement pattern fill in Vello
// For now, just fill with a semi-transparent version of the color // For now, just fill with a semi-transparent version of the color
let path = self.push_path(subpaths, transform); 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 // TODO: Implement proper text measurement in Vello
0. 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 // TODO: Implement text rendering in Vello
} }
pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) { fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) {
if translation.x.abs() > 1e-3 { if translation.x.abs() > 1e-3 {
self.dashed_line(quad.top_left(), quad.top_right(), None, None, Some(2.), Some(2.), Some(0.5)); 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<SegmentId>),
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
None,
}

View file

@ -158,11 +158,9 @@ impl WgpuExecutor {
}); });
let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 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 { let render_params = RenderParams {
// We are using an explicit opaque color here to eliminate the alpha premultiplication step base_color: vello::peniko::Color::from_rgba8(r, g, b, a),
// which would be required to support a transparent webgpu canvas
base_color: vello::peniko::Color::from_rgba8(r, g, b, 0xff),
width: size.x, width: size.x,
height: size.y, height: size.y,
antialiasing_method: AaConfig::Msaa16, antialiasing_method: AaConfig::Msaa16,