From 7c3fed8ef6d7bfe4b7b63f800954372ca0c618f8 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 22:53:43 +0200 Subject: [PATCH 01/18] wip --- .../src/rendering/image_renderer.rs | 17 ++++++++- src/webview-ui/src/shaders/image-uint.frag | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index 5bce314e..dfd1ab48 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -475,6 +475,11 @@ impl ImageRenderer { None }; + let texture_size = Vec2::new(texture.image_size().width, texture.image_size().height); + uniform_values.insert("u_texture_size", UniformValue::Vec2(&texture_size)); + let only_edges = true; + uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); + if texture_info.channels == Channels::One { if let Some(ref clip_min) = drawing_options.clip.min { uniform_values.insert("u_clip_min", UniformValue::Bool(&true)); @@ -551,7 +556,11 @@ impl ImageRenderer { &drawing_options, ); - text_color(pixel_color, &DrawingOptions::default()) + if only_edges { + pixel_color + } else { + text_color(pixel_color, &DrawingOptions::default()) + } } _ => { let rgba = Vec4::from(pixel_value.as_rgba_f32()); @@ -559,7 +568,11 @@ impl ImageRenderer { * (rgba / coloring_factors.normalization_factor) + coloring_factors.color_addition; - text_color(pixel_color, &drawing_options) + if only_edges { + pixel_color + } else { + text_color(pixel_color, &drawing_options) + } } }; diff --git a/src/webview-ui/src/shaders/image-uint.frag b/src/webview-ui/src/shaders/image-uint.frag index f67cdc0a..c79c6401 100644 --- a/src/webview-ui/src/shaders/image-uint.frag +++ b/src/webview-ui/src/shaders/image-uint.frag @@ -7,6 +7,7 @@ in vec2 vout_uv; layout(location = 0) out vec4 fout_color; uniform usampler2D u_texture; +uniform vec2 u_texture_size; // drawing options uniform float u_normalization_factor; @@ -17,6 +18,7 @@ uniform bool u_clip_min; uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +uniform bool u_only_edges; uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -24,6 +26,9 @@ uniform sampler2D u_colormap; uniform vec2 u_buffer_dimension; uniform bool u_enable_borders; +// Thickness of the edge as a fraction of the pixel size +const float EDGE_THICKNESS = 0.2; + const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; @@ -42,6 +47,39 @@ void main() { vec4 sampled = vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); + if (u_only_edges) { + // Calculate the size of one pixel in texture coordinates + vec2 texel_size = 1.0 / u_texture_size; + + // Sample the current pixel and its neighbors + uint current = texture(u_texture, vout_uv).r; + uint top = texture(u_texture, vout_uv - vec2(0.0, texel_size.y)).r; + uint bottom = texture(u_texture, vout_uv + vec2(0.0, texel_size.y)).r; + uint left = texture(u_texture, vout_uv - vec2(texel_size.x, 0.0)).r; + uint right = texture(u_texture, vout_uv + vec2(texel_size.x, 0.0)).r; + + bool is_left_border = (current != left); + bool is_right_border = (current != right); + bool is_top_border = (current != top); + bool is_bottom_border = (current != bottom); + + // Calculate the position within the pixel + vec2 pixel_position = fract(vout_uv * u_texture_size); + + bool is_top_edge = pixel_position.y < EDGE_THICKNESS && is_top_border; + bool is_bottom_edge = + pixel_position.y > (1.0 - EDGE_THICKNESS) && is_bottom_border; + bool is_left_edge = pixel_position.x < EDGE_THICKNESS && is_left_border; + bool is_right_edge = + pixel_position.x > (1.0 - EDGE_THICKNESS) && is_right_border; + + if (is_top_edge || is_bottom_edge || is_left_edge || is_right_edge) { + + } else { + sampled = vec4(0.0, 0.0, 0.0, 1.0); + } + } + if (u_clip_min) { sampled = vec4(max(sampled.r, u_min_clip_value), max(sampled.g, u_min_clip_value), From b49ce25c0d9f602391db3b9d3ac266ec43b18c5c Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 22:55:10 +0200 Subject: [PATCH 02/18] WIP --- src/webview-ui/main.css | 25 ++++++ src/webview-ui/src/app.rs | 27 ++++++- src/webview-ui/src/components/context_menu.rs | 42 ++++++++++ .../src/components/context_menu_view.rs | 79 +++++++++++++++++++ .../src/components/image_list_item.rs | 53 ++++++++++++- .../src/components/image_selection_list.rs | 7 +- src/webview-ui/src/components/mod.rs | 2 + .../src/rendering/image_renderer.rs | 8 +- .../src/rendering/rendering_context.rs | 1 + src/webview-ui/src/shaders/image-uint.frag | 5 +- 10 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 src/webview-ui/src/components/context_menu.rs create mode 100644 src/webview-ui/src/components/context_menu_view.rs diff --git a/src/webview-ui/main.css b/src/webview-ui/main.css index cfdb56e2..1d128f0a 100644 --- a/src/webview-ui/main.css +++ b/src/webview-ui/main.css @@ -495,3 +495,28 @@ input.vscode-textfield[type='file']::file-selector-button:hover, margin: 0; height: 100%; } + + +.context-menu { + position: fixed; + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + border-radius: 5px; + padding: 8px; + list-style: none; + margin: 0; + z-index: 1000; +} + +.context-menu-item { + color: var(--vscode-menu-foreground); + padding: 4px 8px; + cursor: pointer; +} + +.context-menu-item:hover { + background-color: var(--vscode-menu-selectionBackground); + color: var(--vscode-menu-selectionForeground); + border-radius: 4px; +} \ No newline at end of file diff --git a/src/webview-ui/src/app.rs b/src/webview-ui/src/app.rs index 44dfb6f4..c2d2633d 100644 --- a/src/webview-ui/src/app.rs +++ b/src/webview-ui/src/app.rs @@ -16,6 +16,8 @@ use crate::common::Size; use crate::common::ValueVariableKind; use crate::common::ViewId; use crate::common::ViewableObjectId; +use crate::components::context_menu::ContextMenuProvider; +use crate::components::context_menu_view::ContextMenu; use crate::components::main::Main; use crate::configurations; use crate::keyboard_event::KeyboardHandler; @@ -32,6 +34,8 @@ use crate::vscode::vscode_listener::VSCodeListener; use crate::vscode::vscode_requests::VSCodeRequests; use crate::webgl_utils; use anyhow::{anyhow, Result}; +use gloo::events::EventListener; +use gloo::events::EventListenerOptions; use itertools::izip; use std::cell::RefCell; use std::rc::Rc; @@ -83,6 +87,7 @@ fn rendering_context() -> impl RenderingContext { .image_views .borrow() .get_currently_viewing(view_id), + overlays: [].to_vec(), } } @@ -358,6 +363,23 @@ pub(crate) fn App() -> Html { } }); + // disable right-click context menu globally + use_effect_with((), |_| { + let document = web_sys::window().unwrap().document().unwrap(); + let listener = EventListener::new_with_options( + &document, + "contextmenu", + EventListenerOptions::enable_prevent_default(), + move |event| { + event.prevent_default(); + }, + ); + + move || { + drop(listener); + } + }); + let main_style = use_style!( r#" @@ -381,7 +403,10 @@ pub(crate) fn App() -> Html { html! {
-
+ +
+ +
} } diff --git a/src/webview-ui/src/components/context_menu.rs b/src/webview-ui/src/components/context_menu.rs new file mode 100644 index 00000000..5bebe6cd --- /dev/null +++ b/src/webview-ui/src/components/context_menu.rs @@ -0,0 +1,42 @@ +use yew::prelude::*; + +/// One menu entry +#[derive(Clone, PartialEq)] +pub struct ContextMenuItem { + pub label: String, + pub disabled: bool, + pub action: Callback<()>, +} + +/// Data to control and display the context menu +#[derive(Clone, PartialEq)] +pub struct ContextMenuData { + pub x: i32, + pub y: i32, + pub items: Vec, +} + +/// Global context type +type ContextMenuState = UseStateHandle>; + +/// Provide the context at the root of the app +#[derive(Properties, PartialEq)] +pub struct ProviderProps { + #[prop_or_default] + pub children: Children, +} + +#[function_component] +pub fn ContextMenuProvider(props: &ProviderProps) -> Html { + let state = use_state(|| None::); + html! { + context={state}> + { for props.children.iter() } + > + } +} + +#[hook] +pub fn use_context_menu() -> UseStateHandle> { + use_context::().expect("ContextMenuProvider is missing") +} diff --git a/src/webview-ui/src/components/context_menu_view.rs b/src/webview-ui/src/components/context_menu_view.rs new file mode 100644 index 00000000..7dad29c0 --- /dev/null +++ b/src/webview-ui/src/components/context_menu_view.rs @@ -0,0 +1,79 @@ +use gloo::events::EventListener; +use wasm_bindgen::JsCast; +use yew::prelude::*; + +use crate::components::context_menu::{use_context_menu, ContextMenuData}; + +#[function_component(ContextMenu)] +pub fn context_menu() -> Html { + let ctx = use_context_menu(); + let menu_ref = use_node_ref(); + + { + let ctx = ctx.clone(); + let menu_ref = menu_ref.clone(); + + use_effect_with((), move |_| { + let document = web_sys::window().unwrap().document().unwrap(); + + // Hide on left click + let click_listener = EventListener::new(&document, "click", { + let ctx = ctx.clone(); + let menu_ref = menu_ref.clone(); + move |e| { + let target = e.target().and_then(|t| t.dyn_into::().ok()); + let inside = match (target, menu_ref.cast::()) { + (Some(t), Some(m)) => m.contains(Some(&t)), + _ => false, + }; + if !inside { + ctx.set(None); + } + } + }); + + // Hide on Escape key + let key_listener = EventListener::new(&web_sys::window().unwrap(), "keydown", { + let ctx = ctx.clone(); + move |e| { + let event = e.dyn_ref::(); + if let Some(k) = event { + if k.key() == "Escape" { + ctx.set(None); + } + } + } + }); + + move || { + drop(click_listener); + drop(key_listener); + } + }); + } + + if let Some(ContextMenuData { x, y, items }) = &*ctx { + html! { +
    + { for items.iter().map(|item| { + let action = item.action.clone(); + let disabled = item.disabled; + html! { +
  • + { &item.label } +
  • + } + })} +
+ } + } else { + html! {} + } +} diff --git a/src/webview-ui/src/components/image_list_item.rs b/src/webview-ui/src/components/image_list_item.rs index 53d90641..8d8ce493 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -6,7 +6,10 @@ use yewdux::Dispatch; use crate::{ application_state::app_state::{AppState, UiAction}, common::{Image, MinimalImageInfo, ValueVariableKind}, - components::display_options::DisplayOption, + components::{ + context_menu::{use_context_menu, ContextMenuData, ContextMenuItem}, + display_options::DisplayOption, + }, vscode::vscode_requests::VSCodeRequests, }; @@ -155,8 +158,54 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { "# ); + let ctx = use_context_menu(); + let name = "Image ".to_string() + &image_id.as_unique_string(); + + let on_context = { + let ctx = ctx.clone(); + let name = name.clone(); + Callback::from(move |e: MouseEvent| { + e.prevent_default(); + ctx.set(Some(ContextMenuData { + x: e.client_x(), + y: e.client_y(), + items: vec![ + ContextMenuItem { + label: "Rename".into(), + action: Callback::from({ + let name = name.clone(); + move |_| { + web_sys::window() + .unwrap() + .alert_with_message(&format!("Rename {}", name)) + .ok(); + } + }), + disabled: false, + }, + ContextMenuItem { + label: "Delete".into(), + action: Callback::from({ + let name = name.clone(); + move |_| { + web_sys::window() + .unwrap() + .alert_with_message(&format!("Delete {}", name)) + .ok(); + } + }), + disabled: false, + }, + ], + })); + }) + }; + html! { -
+
{pin_unpin_button} diff --git a/src/webview-ui/src/components/image_selection_list.rs b/src/webview-ui/src/components/image_selection_list.rs index 92a578fc..8d07d0f5 100644 --- a/src/webview-ui/src/components/image_selection_list.rs +++ b/src/webview-ui/src/components/image_selection_list.rs @@ -106,7 +106,12 @@ fn ImageItemWrapper(props: &ImageItemWrapperProps) -> Html { {onclick} class={entry_style.clone()} > - +
} } diff --git a/src/webview-ui/src/components/mod.rs b/src/webview-ui/src/components/mod.rs index 5485fe2a..56449892 100644 --- a/src/webview-ui/src/components/mod.rs +++ b/src/webview-ui/src/components/mod.rs @@ -18,5 +18,7 @@ pub(crate) mod status_bar; mod types; pub(crate) mod view_container; pub(crate) mod viewable_info_container; +pub(crate) mod context_menu; +pub(crate) mod context_menu_view; pub(crate) use types::ToggleState; diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index dfd1ab48..c2971d71 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -477,8 +477,6 @@ impl ImageRenderer { let texture_size = Vec2::new(texture.image_size().width, texture.image_size().height); uniform_values.insert("u_texture_size", UniformValue::Vec2(&texture_size)); - let only_edges = true; - uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); if texture_info.channels == Channels::One { if let Some(ref clip_min) = drawing_options.clip.min { @@ -511,6 +509,12 @@ impl ImageRenderer { set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); + let only_edges = true; + uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); + + set_uniforms(program, &uniform_values); + draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); + let to_render_text = pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_values as _; if to_render_text { diff --git a/src/webview-ui/src/rendering/rendering_context.rs b/src/webview-ui/src/rendering/rendering_context.rs index 0cd1f8f7..b2c284d4 100644 --- a/src/webview-ui/src/rendering/rendering_context.rs +++ b/src/webview-ui/src/rendering/rendering_context.rs @@ -18,6 +18,7 @@ use crate::{ pub(crate) struct ImageViewData { pub html_element: HtmlElement, pub currently_viewing: Option, + pub overlays: Vec, pub camera: camera::Camera, } diff --git a/src/webview-ui/src/shaders/image-uint.frag b/src/webview-ui/src/shaders/image-uint.frag index c79c6401..0ccdee79 100644 --- a/src/webview-ui/src/shaders/image-uint.frag +++ b/src/webview-ui/src/shaders/image-uint.frag @@ -76,8 +76,11 @@ void main() { if (is_top_edge || is_bottom_edge || is_left_edge || is_right_edge) { } else { - sampled = vec4(0.0, 0.0, 0.0, 1.0); + // sampled = vec4(0.0, 0.0, 0.0, 0.0); + discard; } + } else { + sampled = vec4(0.2, 0.2, 0.2, 1.0); } if (u_clip_min) { From c267fbbeac64afe9d9ccd2d4f4e1faee39bbb901 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 22:59:50 +0200 Subject: [PATCH 03/18] Add shader management and rendering support --- src/webview-ui/.gitignore | 2 + src/webview-ui/Cargo.toml | 3 + src/webview-ui/build.rs | 52 ++++ src/webview-ui/debug_shaders/int-image.frag | 178 +---------- .../debug_shaders/int-planar-image.frag | 286 +----------------- .../debug_shaders/normalized-image.frag | 160 +--------- .../normalized-planar-image.frag | 286 +----------------- src/webview-ui/debug_shaders/uint-image.frag | 178 +---------- .../debug_shaders/uint-planar-image.frag | 286 +----------------- src/webview-ui/shaders/Cargo.toml | 7 + src/webview-ui/shaders/src/lib.rs | 53 ++++ src/webview-ui/shaders/src/shader_parts.rs | 279 +++++++++++++++++ src/webview-ui/src/lib.rs | 2 +- .../src/rendering/image_renderer.rs | 48 +-- webpack.webview.config.mjs | 4 + 15 files changed, 461 insertions(+), 1363 deletions(-) create mode 100644 src/webview-ui/build.rs create mode 100644 src/webview-ui/shaders/Cargo.toml create mode 100644 src/webview-ui/shaders/src/lib.rs create mode 100644 src/webview-ui/shaders/src/shader_parts.rs diff --git a/src/webview-ui/.gitignore b/src/webview-ui/.gitignore index c76f8ee7..f2d271b0 100644 --- a/src/webview-ui/.gitignore +++ b/src/webview-ui/.gitignore @@ -16,3 +16,5 @@ Cargo.lock # wasm-pack pkg/ + +/debug_shaders \ No newline at end of file diff --git a/src/webview-ui/Cargo.toml b/src/webview-ui/Cargo.toml index 62766a7c..6f25dc18 100644 --- a/src/webview-ui/Cargo.toml +++ b/src/webview-ui/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" rust-version = "1.80" resolver = "2" +[build-dependencies] +shaders = { path = "shaders" } + [lib] crate-type = [ "cdylib" ] diff --git a/src/webview-ui/build.rs b/src/webview-ui/build.rs new file mode 100644 index 00000000..f6968a81 --- /dev/null +++ b/src/webview-ui/build.rs @@ -0,0 +1,52 @@ +use std::{env, fs, io::Write, path::PathBuf}; + +fn base_output() -> PathBuf { + PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders") +} + +fn is_debug() -> bool { + match env::var("PROFILE") { + Ok(profile) => profile == "debug", + _ => false, + } +} + +fn create_shader_file(content: &str, filename: &str) { + let base_output = base_output(); + + let file_path = base_output.join(filename); + let mut file = fs::File::create(file_path).expect("Unable to create file"); + file.write_all(content.as_bytes()) + .expect("Unable to write to file"); + + // Also write to project_root/debug_shaders in debug mode + if is_debug() { + let root_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let debug_dir = root_dir.join("debug_shaders"); + fs::create_dir_all(&debug_dir).unwrap(); + + let debug_file = debug_dir.join(filename); + fs::write(&debug_file, content).unwrap(); + } +} + +fn main() { + let base_output = base_output(); + println!("cargo:rustc-env=SHADERS_DIR={}", base_output.display()); + fs::create_dir_all(&base_output).expect("Unable to create directory"); + + #[rustfmt::skip] + create_shader_file(shaders::UINT_FRAGMENT_SHADER, "uint-image.frag"); + #[rustfmt::skip] + create_shader_file(shaders::INT_FRAGMENT_SHADER, "int-image.frag"); + #[rustfmt::skip] + create_shader_file(shaders::NORMALIZED_FRAGMENT_SHADER, "normalized-image.frag"); + #[rustfmt::skip] + create_shader_file(shaders::UINT_PLANAR_FRAGMENT_SHADER, "uint-planar-image.frag"); + #[rustfmt::skip] + create_shader_file(shaders::INT_PLANAR_FRAGMENT_SHADER, "int-planar-image.frag"); + #[rustfmt::skip] + create_shader_file(shaders::NORMALIZED_PLANAR_FRAGMENT_SHADER, "normalized-planar-image.frag"); + + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/src/webview-ui/debug_shaders/int-image.frag b/src/webview-ui/debug_shaders/int-image.frag index dec6dac3..2358946e 100644 --- a/src/webview-ui/debug_shaders/int-image.frag +++ b/src/webview-ui/debug_shaders/int-image.frag @@ -22,12 +22,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -38,9 +32,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -71,149 +62,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -271,23 +119,11 @@ sampled = } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -299,12 +135,8 @@ sampled = color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/debug_shaders/int-planar-image.frag b/src/webview-ui/debug_shaders/int-planar-image.frag index 95691f51..bb751035 100644 --- a/src/webview-ui/debug_shaders/int-planar-image.frag +++ b/src/webview-ui/debug_shaders/int-planar-image.frag @@ -27,12 +27,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -43,9 +37,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -76,257 +67,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -396,23 +136,11 @@ if ((need & NEED_ALPHA) != 0) { } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -424,12 +152,8 @@ if ((need & NEED_ALPHA) != 0) { color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/debug_shaders/normalized-image.frag b/src/webview-ui/debug_shaders/normalized-image.frag index cdb6db70..63228a77 100644 --- a/src/webview-ui/debug_shaders/normalized-image.frag +++ b/src/webview-ui/debug_shaders/normalized-image.frag @@ -21,12 +21,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -37,9 +31,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -70,131 +61,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -sampled = texture(u_texture, pix); - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -250,23 +116,11 @@ sampled = texture(u_texture, pix); } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -278,12 +132,8 @@ sampled = texture(u_texture, pix); color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/debug_shaders/normalized-planar-image.frag b/src/webview-ui/debug_shaders/normalized-planar-image.frag index 62340fb6..f42f248b 100644 --- a/src/webview-ui/debug_shaders/normalized-planar-image.frag +++ b/src/webview-ui/debug_shaders/normalized-planar-image.frag @@ -26,12 +26,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -42,9 +36,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -75,257 +66,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -395,23 +135,11 @@ void main() { } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -423,12 +151,8 @@ void main() { color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/debug_shaders/uint-image.frag b/src/webview-ui/debug_shaders/uint-image.frag index a8a8a797..ca26b55e 100644 --- a/src/webview-ui/debug_shaders/uint-image.frag +++ b/src/webview-ui/debug_shaders/uint-image.frag @@ -22,12 +22,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -38,9 +32,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -71,149 +62,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -271,23 +119,11 @@ sampled = } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -299,12 +135,8 @@ sampled = color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/debug_shaders/uint-planar-image.frag b/src/webview-ui/debug_shaders/uint-planar-image.frag index 7a1d7807..c3a8d6ca 100644 --- a/src/webview-ui/debug_shaders/uint-planar-image.frag +++ b/src/webview-ui/debug_shaders/uint-planar-image.frag @@ -27,12 +27,6 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; -uniform bool u_zeros_as_transparent; -uniform bool u_edges_only; - uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -43,9 +37,6 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - const int NEED_RED = 1; @@ -76,257 +67,6 @@ bool is_nan(float val) { return (val < 0. || 0. < val || val == 0.) ? false : true; } -bool is_edge(vec2 uv) { - // Calculate the size of one pixel in texture coordinates - vec2 texel_size = 1.0 / u_buffer_dimension; - - // Sample the current pixel and its neighbors - float current; - { - vec2 pix = uv; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top; - { - vec2 pix = uv - vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom; - { - vec2 pix = uv + vec2(0.0, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float left; - { - vec2 pix = uv - vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float right; - { - vec2 pix = uv + vec2(texel_size.x, 0.0); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_left; - { - vec2 pix = uv - texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float top_right; - { - vec2 pix = uv + vec2(texel_size.x, -texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_left; - { - vec2 pix = uv + vec2(-texel_size.x, texel_size.y); - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - float bottom_right; - { - vec2 pix = uv + texel_size; - vec4 sampled = vec4(0., 0., 0., 1.); - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE - } - - bool is_left_border = (current != left); - bool is_right_border = (current != right); - bool is_top_border = (current != top); - bool is_bottom_border = (current != bottom); - bool is_top_left_border = (current != top_left); - bool is_top_right_border = (current != top_right); - bool is_bottom_left_border = (current != bottom_left); - bool is_bottom_right_border = (current != bottom_right); - - // Calculate the position within the pixel - vec2 pixel_position = fract(uv * u_buffer_dimension); - // New: compute image-pixel size in screen-pixels and a dynamic threshold. - float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); - float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); - - bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; - bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; - bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; - bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_left_border; - bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - (pixel_position.y * inv_derivative < dynamic_thickness)) && - is_top_right_border; - bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_left_border; - bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && - ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && - is_bottom_right_border; - - // Return true if any edge condition is met - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || - is_top_left_edge || is_top_right_edge || is_bottom_left_edge || - is_bottom_right_edge; -} - @@ -396,23 +136,11 @@ if ((need & NEED_ALPHA) != 0) { } } - if (u_edges_only) { - if (!is_edge(vout_uv)) { - color = vec4(0.0, 0.0, 0.0, 1.0); - } - } - - if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) { - color.a = 0.0; - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders && !u_is_overlay) { + if (u_enable_borders) { float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -424,12 +152,8 @@ if ((need & NEED_ALPHA) != 0) { color.rgb += vec3(vertical_border + horizontal_border); } - if (u_is_overlay) { - color.a = u_overlay_alpha * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; fout_color = color; } diff --git a/src/webview-ui/shaders/Cargo.toml b/src/webview-ui/shaders/Cargo.toml new file mode 100644 index 00000000..0da1190c --- /dev/null +++ b/src/webview-ui/shaders/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "shaders" +version = "0.1.0" +edition = "2021" + +[dependencies] +const_format = "0.2.34" diff --git a/src/webview-ui/shaders/src/lib.rs b/src/webview-ui/shaders/src/lib.rs new file mode 100644 index 00000000..020c3802 --- /dev/null +++ b/src/webview-ui/shaders/src/lib.rs @@ -0,0 +1,53 @@ +mod shader_parts; +use shader_parts::*; + +pub const NORMALIZED_FRAGMENT_SHADER: &str = create_fragment_shader!( + NORMALIZED_HEADER, + NORMALIZED_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + NORMALIZED_SAMPLE +); +pub const UINT_FRAGMENT_SHADER: &str = create_fragment_shader!( + UINT_HEADER, + UINT_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + UINT_SAMPLE +); + +pub const INT_FRAGMENT_SHADER: &str = create_fragment_shader!( + INT_HEADER, + INT_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + INT_SAMPLE +); + +pub const NORMALIZED_PLANAR_FRAGMENT_SHADER: &str = create_fragment_shader!( + NORMALIZED_HEADER, + NORMALIZED_PLANAR_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + NORMALIZED_PLANAR_SAMPLE +); +pub const UINT_PLANAR_FRAGMENT_SHADER: &str = create_fragment_shader!( + UINT_HEADER, + UINT_PLANAR_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + INTEGER_PLANAR_SAMPLE +); +pub const INT_PLANAR_FRAGMENT_SHADER: &str = create_fragment_shader!( + INT_HEADER, + INT_PLANAR_TEXTURES, + "", + PLANAR_CONSTANTS, + "", + INTEGER_PLANAR_SAMPLE +); diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs new file mode 100644 index 00000000..4bc0714c --- /dev/null +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -0,0 +1,279 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] + +macro_rules! create_fragment_shader { + ( + $header:expr, + $textures:expr, + $additional_constants:expr, + $additional_uniforms:expr, + $additional_functions:expr, + $sample_code:expr + ) => { + const_format::formatcp!( +/*glsl*/ r"#version 300 es +{HEADER} + +in vec2 vout_uv; +layout(location = 0) out vec4 fout_color; + +{TEXTURES} + +// drawing options +uniform float u_normalization_factor; +uniform mat4 u_color_multiplier; +uniform vec4 u_color_addition; +uniform bool u_invert; +uniform bool u_clip_min; +uniform bool u_clip_max; +uniform float u_min_clip_value; +uniform float u_max_clip_value; + +uniform bool u_use_colormap; +uniform sampler2D u_colormap; + +uniform vec2 u_buffer_dimension; +uniform bool u_enable_borders; + +const float CHECKER_SIZE = 10.0; +const float WHITE_CHECKER = 0.9; +const float BLACK_CHECKER = 0.6; + +{ADDITIONAL_CONSTANTS} +{ADDITIONAL_UNIFORMS} + +float checkboard(vec2 st) {{ + vec2 pos = mod(st, CHECKER_SIZE * 2.0); + float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); + return mix(BLACK_CHECKER, WHITE_CHECKER, value); +}} + +bool is_nan(float val) {{ + return (val < 0. || 0. < val || val == 0.) ? false : true; +}} + +{ADDITIONAL_FUNCTIONS} + + +void main() {{ + vec2 pix = vout_uv; + + vec4 sampled = vec4(0., 0., 0., 1.); + {{ + {SAMPLE_CODE} + }} + + vec4 color; + if ( + is_nan(sampled.r) || + is_nan(sampled.g) || + is_nan(sampled.b) || + is_nan(sampled.a) + ) {{ + + color = vec4(0., 0., 0., 1.); + + if (u_invert) {{ + color.rgb = 1. - color.rgb; + }} + + }} else {{ + if (u_clip_min) {{ + sampled = vec4(max(sampled.r, u_min_clip_value), + max(sampled.g, u_min_clip_value), + max(sampled.b, u_min_clip_value), sampled.a); + }} + if (u_clip_max) {{ + sampled = vec4(min(sampled.r, u_max_clip_value), + min(sampled.g, u_max_clip_value), + min(sampled.b, u_max_clip_value), sampled.a); + }} + + color = u_color_multiplier * (sampled / u_normalization_factor) + + u_color_addition; + + color = clamp(color, 0.0, 1.0); + + if (u_invert) {{ + color.rgb = 1. - color.rgb; + }} + + if (u_use_colormap) {{ + vec2 colormap_uv = vec2(color.r, 0.5); + vec4 colormap_color = texture(u_colormap, colormap_uv); + color.rgb = colormap_color.rgb; + }} + }} + + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + + vec2 buffer_position = vout_uv * u_buffer_dimension; + if (u_enable_borders) {{ + float alpha = + max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); + float x_ = fract(buffer_position.x); + float y_ = fract(buffer_position.y); + float vertical_border = + clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); + float horizontal_border = + clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); + color.rgb += vec3(vertical_border + horizontal_border); + }} + + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + + fout_color = color; +}} + +", + HEADER = $header, + TEXTURES = $textures, + ADDITIONAL_CONSTANTS = $additional_constants, + ADDITIONAL_UNIFORMS = $additional_uniforms, + ADDITIONAL_FUNCTIONS = $additional_functions, + SAMPLE_CODE = $sample_code + ) + }; +} + +pub(crate) use create_fragment_shader; + + +/** + * Headers + */ +pub(crate) const NORMALIZED_HEADER: &str = /*glsl*/ r" +precision highp float; +precision highp sampler2D; +"; + +pub(crate) const UINT_HEADER: &str = /*glsl*/ r" +precision highp float; +precision highp int; +precision highp usampler2D; +"; + +pub(crate) const INT_HEADER: &str = /*glsl*/ r" +precision highp float; +precision highp int; +precision highp isampler2D; +"; + +/** + * Textures + */ +pub(crate) const NORMALIZED_TEXTURES: &str = /*glsl*/ r" +uniform sampler2D u_texture; +"; +pub(crate) const UINT_TEXTURES: &str = /*glsl*/ r" +uniform usampler2D u_texture; +"; +pub(crate) const INT_TEXTURES: &str = /*glsl*/ r" +uniform isampler2D u_texture; +"; +pub(crate) const NORMALIZED_PLANAR_TEXTURES: &str = /*glsl*/ r" +uniform int u_image_type; + +uniform sampler2D u_texture_r; +uniform sampler2D u_texture_g; +uniform sampler2D u_texture_b; +uniform sampler2D u_texture_a; +"; +pub(crate) const UINT_PLANAR_TEXTURES: &str = /*glsl*/ r" +uniform int u_image_type; + +uniform usampler2D u_texture_r; +uniform usampler2D u_texture_g; +uniform usampler2D u_texture_b; +uniform usampler2D u_texture_a; +"; + +pub(crate) const INT_PLANAR_TEXTURES: &str = /*glsl*/ r" +uniform int u_image_type; + +uniform isampler2D u_texture_r; +uniform isampler2D u_texture_g; +uniform isampler2D u_texture_b; +uniform isampler2D u_texture_a; +"; + +/** + * Constants + */ +pub(crate) const PLANAR_CONSTANTS: &str = /*glsl*/ r" +const int NEED_RED = 1; +const int NEED_GREEN = 2; +const int NEED_BLUE = 4; +const int NEED_ALPHA = 8; + +const int IMAGE_TYPE_GRAYSCALE = 0; +const int IMAGE_TYPE_RGB = 1; +const int IMAGE_TYPE_RGBA = 2; +const int IMAGE_TYPE_GA = 3; + +const int TYPE_TO_NEED[4] = int[]( + NEED_RED, + NEED_RED | NEED_GREEN | NEED_BLUE, + NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, + NEED_RED | NEED_GREEN +); +"; + + +/** + * Sampler + */ +// Works for both int and uint +pub(crate) const INTEGER_PLANAR_SAMPLE: &str = /*glsl*/ r" + +int need = TYPE_TO_NEED[u_image_type]; +if ((need & NEED_RED) != 0) { + sampled.r = float(texture(u_texture_r, pix).r); +} +if ((need & NEED_GREEN) != 0) { + sampled.g = float(texture(u_texture_g, pix).r); +} +if ((need & NEED_BLUE) != 0) { + sampled.b = float(texture(u_texture_b, pix).r); +} +if ((need & NEED_ALPHA) != 0) { + sampled.a = float(texture(u_texture_a, pix).r); +} + +"; + +pub(crate) const NORMALIZED_PLANAR_SAMPLE: &str = /*glsl*/ r" + + int need = TYPE_TO_NEED[u_image_type]; + if ((need & NEED_RED) != 0) { + sampled.r = texture(u_texture_r, pix).r; + } + if ((need & NEED_GREEN) != 0) { + sampled.g = texture(u_texture_g, pix).r; + } + if ((need & NEED_BLUE) != 0) { + sampled.b = texture(u_texture_b, pix).r; + } + if ((need & NEED_ALPHA) != 0) { + sampled.a = texture(u_texture_a, pix).r; + } + + "; + +pub(crate) const UINT_SAMPLE: &str = /*glsl*/ r" +uvec4 texel = texture(u_texture, pix); +sampled = + vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); +"; + +pub(crate) const INT_SAMPLE: &str = /*glsl*/ r" +ivec4 texel = texture(u_texture, pix); +sampled = + vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); +"; + +pub(crate) const NORMALIZED_SAMPLE: &str = /*glsl*/ r" +sampled = texture(u_texture, pix); +"; + diff --git a/src/webview-ui/src/lib.rs b/src/webview-ui/src/lib.rs index 20c06db8..b2a3b9b6 100644 --- a/src/webview-ui/src/lib.rs +++ b/src/webview-ui/src/lib.rs @@ -14,6 +14,7 @@ mod colormap; mod common; mod components; mod configurations; +mod hooks; mod keyboard_event; mod math_utils; mod mouse_events; @@ -22,7 +23,6 @@ mod rendering; mod tmp_for_debug; mod vscode; mod webgl_utils; -mod hooks; use app::App; use cfg_if::cfg_if; diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index c2971d71..57dee177 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -36,6 +36,12 @@ use crate::rendering::pixel_text_rendering::{ PixelTextCache, PixelTextRenderer, PixelTextRenderingData, }; +macro_rules! include_shader { + ($shader_name:expr) => { + include_str!(concat!(env!("OUT_DIR"), "/shaders/", $shader_name)) + }; +} + struct Programs { normalized_image: ProgramBundle, uint_image: ProgramBundle, @@ -163,6 +169,12 @@ impl ImageRenderer { let gl = rendering_context.gl().clone(); gl.enable(WebGl2RenderingContext::SCISSOR_TEST); + gl.enable(WebGl2RenderingContext::BLEND); + gl.blend_func( + WebGl2RenderingContext::SRC_ALPHA, + WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA, + ); + gl.depth_mask(false); let programs = ImageRenderer::create_programs(&gl).unwrap(); @@ -197,32 +209,32 @@ impl ImageRenderer { fn create_programs(gl: &WebGl2RenderingContext) -> Result { let normalized_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-normalized.frag")) + .fragment_shader(include_shader!("normalized-image.frag")) .attribute("vin_position") .build()?; let uint_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-uint.frag")) + .fragment_shader(include_shader!("uint-image.frag")) .attribute("vin_position") .build()?; let int_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-int.frag")) + .fragment_shader(include_shader!("int-image.frag")) .attribute("vin_position") .build()?; let planar_normalized_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-planar-normalized.frag")) + .fragment_shader(include_shader!("normalized-planar-image.frag")) .attribute("vin_position") .build()?; let planar_uint_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-planar-uint.frag")) + .fragment_shader(include_shader!("uint-planar-image.frag")) .attribute("vin_position") .build()?; let planar_int_image = webgl_utils::program::GLProgramBuilder::create(gl) .vertex_shader(include_str!("../shaders/image.vert")) - .fragment_shader(include_str!("../shaders/image-planar-int.frag")) + .fragment_shader(include_shader!("int-planar-image.frag")) .attribute("vin_position") .build()?; @@ -509,11 +521,11 @@ impl ImageRenderer { set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); - let only_edges = true; - uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); + // let only_edges = true; + // uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); - set_uniforms(program, &uniform_values); - draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); + // set_uniforms(program, &uniform_values); + // draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); let to_render_text = pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_values as _; @@ -560,11 +572,11 @@ impl ImageRenderer { &drawing_options, ); - if only_edges { - pixel_color - } else { + // if only_edges { + // pixel_color + // } else { text_color(pixel_color, &DrawingOptions::default()) - } + // } } _ => { let rgba = Vec4::from(pixel_value.as_rgba_f32()); @@ -572,11 +584,11 @@ impl ImageRenderer { * (rgba / coloring_factors.normalization_factor) + coloring_factors.color_addition; - if only_edges { - pixel_color - } else { + // if only_edges { + // pixel_color + // } else { text_color(pixel_color, &drawing_options) - } + // } } }; diff --git a/webpack.webview.config.mjs b/webpack.webview.config.mjs index 0979f33b..b0769c14 100644 --- a/webpack.webview.config.mjs +++ b/webpack.webview.config.mjs @@ -82,6 +82,10 @@ const WebviewConfig = { crateDirectory: webviewPath, outDir: path.resolve(webviewPath, 'pkg'), outName: 'webview', + watchDirectories: [ + path.resolve(webviewPath, "shaders"), + path.resolve(webviewPath, "src"), + ], }), // Have this example work in Edge which doesn't ship `TextEncoder` or // `TextDecoder` at this time. From 2833ccab1aaecbca330404c4f935f79e501d3227 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 22:59:50 +0200 Subject: [PATCH 04/18] wip: major refactor in image renderer --- ...-image-for-python-debugging.code-workspace | 1 + src/webview-ui/src/app.rs | 41 +- .../src/application_state/app_state.rs | 32 ++ src/webview-ui/src/application_state/views.rs | 52 +- .../src/components/image_list_item.rs | 54 +- .../src/rendering/image_renderer.rs | 479 +++++++++++------- src/webview-ui/src/webgl_utils/program.rs | 16 + src/webview-ui/src/webgl_utils/types.rs | 9 + 8 files changed, 441 insertions(+), 243 deletions(-) diff --git a/simply-view-image-for-python-debugging.code-workspace b/simply-view-image-for-python-debugging.code-workspace index fd6db61a..3701362d 100644 --- a/simply-view-image-for-python-debugging.code-workspace +++ b/simply-view-image-for-python-debugging.code-workspace @@ -23,6 +23,7 @@ "rust": "html", }, "rust-analyzer.check.command": "clippy", + "rust-analyzer.semanticHighlighting.strings.enable": false, "cSpell.words": [ "colorbar" ], diff --git a/src/webview-ui/src/app.rs b/src/webview-ui/src/app.rs index c2d2633d..4046ef70 100644 --- a/src/webview-ui/src/app.rs +++ b/src/webview-ui/src/app.rs @@ -68,26 +68,29 @@ fn rendering_context() -> impl RenderingContext { fn view_data(&self, view_id: ViewId) -> ImageViewData { let dispatch = Dispatch::::global(); + let state = dispatch.get(); + let currently_viewing = state.image_views.borrow().get_currently_viewing(view_id); + let overlays = currently_viewing.as_ref().map_or(Vec::new(), |cv| { + state.overlays.borrow().get_overlays(view_id, cv.id()) + }); + let camera = state.view_cameras.borrow().get(view_id); + let html_element = state + .image_views + .borrow() + .get_node_ref(view_id) + .cast::() + .unwrap_or_else(|| { + panic!( + "Unable to cast node ref to HtmlElement for view {:?}", + view_id + ) + }); + ImageViewData { - camera: dispatch.get().view_cameras.borrow().get(view_id), - html_element: dispatch - .get() - .image_views - .borrow() - .get_node_ref(view_id) - .cast::() - .unwrap_or_else(|| { - panic!( - "Unable to cast node ref to HtmlElement for view {:?}", - view_id - ) - }), - currently_viewing: dispatch - .get() - .image_views - .borrow() - .get_currently_viewing(view_id), - overlays: [].to_vec(), + camera, + html_element, + currently_viewing, + overlays, } } diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index cc3e6636..03ce8eba 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -3,6 +3,7 @@ use super::images::{ImageCache, Images, ImagesDrawingOptions}; use super::sessions::Sessions; use super::views::ImageViews; use super::vscode_data_fetcher::ImagesFetcher; +use crate::application_state::views::Overlays; use crate::coloring::{Clip, Coloring, DrawingOptions}; use crate::common::camera::ViewsCameras; use crate::common::texture_image::TextureImage; @@ -52,6 +53,7 @@ pub(crate) struct AppState { pub image_cache: Mrc, pub drawing_options: Mrc, pub global_drawing_options: GlobalDrawingOptions, + pub overlays: Mrc, pub color_map_registry: Mrc, pub color_map_textures_cache: Mrc, @@ -75,6 +77,7 @@ impl Default for AppState { image_cache: Default::default(), drawing_options: Default::default(), global_drawing_options: Default::default(), + overlays: Default::default(), color_map_registry: Default::default(), color_map_textures_cache: Default::default(), view_cameras: Default::default(), @@ -504,3 +507,32 @@ impl Reducer for UiAction { app_state } } + +pub(crate) enum OverlayAction { + Add { + view_id: ViewId, + image_id: ViewableObjectId, + overlay_id: ViewableObjectId, + }, +} + +impl Reducer for OverlayAction { + fn apply(self, mut app_state: Rc) -> Rc { + let state = Rc::make_mut(&mut app_state); + + match self { + OverlayAction::Add { + view_id, + image_id, + overlay_id, + } => { + state + .overlays + .borrow_mut() + .add_overlay(view_id, image_id, overlay_id); + } + } + + app_state + } +} diff --git a/src/webview-ui/src/application_state/views.rs b/src/webview-ui/src/application_state/views.rs index e57601c6..d16340c1 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, iter::FromIterator}; +use std::{ + collections::{HashMap, HashSet}, + iter::FromIterator, +}; use web_sys::{CustomEvent, HtmlElement}; use yew::NodeRef; @@ -86,3 +89,50 @@ impl Default for ImageViews { Self::new() } } + +#[derive(Debug, Default)] +pub(crate) struct Overlays { + overlays: HashMap<(ViewId, ViewableObjectId), HashSet>, +} + +impl Overlays { + pub(crate) fn add_overlay( + &mut self, + view_id: ViewId, + image_id: ViewableObjectId, + overlay_id: ViewableObjectId, + ) { + self.overlays + .entry((view_id, image_id)) + .or_default() + .insert(overlay_id); + } + + pub(crate) fn remove_overlay( + &mut self, + view_id: ViewId, + image_id: &ViewableObjectId, + overlay_id: &ViewableObjectId, + ) { + if let Some(overlays) = self.overlays.get_mut(&(view_id, image_id.clone())) { + overlays.retain(|id| id != overlay_id); + } + } + + pub(crate) fn get_overlays( + &self, + view_id: ViewId, + image_id: &ViewableObjectId, + ) -> Vec { + self.overlays + .get(&(view_id, image_id.clone())) + .cloned() + .unwrap_or_default() + .into_iter() + .collect() + } + + pub(crate) fn clear_overlays(&mut self, view_id: ViewId, image_id: &ViewableObjectId) { + self.overlays.remove(&(view_id, image_id.clone())); + } +} diff --git a/src/webview-ui/src/components/image_list_item.rs b/src/webview-ui/src/components/image_list_item.rs index 8d8ce493..3e81a484 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -4,8 +4,8 @@ use yew::prelude::*; use yewdux::Dispatch; use crate::{ - application_state::app_state::{AppState, UiAction}, - common::{Image, MinimalImageInfo, ValueVariableKind}, + application_state::app_state::{AppState, OverlayAction, UiAction}, + common::{Image, MinimalImageInfo, ValueVariableKind, ViewId}, components::{ context_menu::{use_context_menu, ContextMenuData, ContextMenuItem}, display_options::DisplayOption, @@ -159,44 +159,36 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { ); let ctx = use_context_menu(); - let name = "Image ".to_string() + &image_id.as_unique_string(); let on_context = { + let image_id = image_id.clone(); let ctx = ctx.clone(); - let name = name.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); ctx.set(Some(ContextMenuData { x: e.client_x(), y: e.client_y(), - items: vec![ - ContextMenuItem { - label: "Rename".into(), - action: Callback::from({ - let name = name.clone(); - move |_| { - web_sys::window() - .unwrap() - .alert_with_message(&format!("Rename {}", name)) - .ok(); + items: vec![ContextMenuItem { + label: "Overlay".into(), + action: Callback::from({ + let image_id = image_id.clone(); + let ctx = ctx.clone(); + move |_| { + let view_id = ViewId::Primary; + let state = Dispatch::::global().get(); + let cv = state.image_views.borrow().get_currently_viewing(view_id); + if let Some(cv) = cv { + Dispatch::::global().apply(OverlayAction::Add { + view_id, + image_id: cv.id().clone(), + overlay_id: image_id.clone(), + }); } - }), - disabled: false, - }, - ContextMenuItem { - label: "Delete".into(), - action: Callback::from({ - let name = name.clone(); - move |_| { - web_sys::window() - .unwrap() - .alert_with_message(&format!("Delete {}", name)) - .ok(); - } - }), - disabled: false, - }, - ], + ctx.set(None); + } + }), + disabled: false, + }], })); }) }; diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index 57dee177..49f49272 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -9,6 +9,8 @@ use glam::{Mat3, UVec2, Vec2, Vec4}; use web_sys::{WebGl2RenderingContext as GL, WebGl2RenderingContext}; +use crate::application_state::app_state::GlobalDrawingOptions; +use crate::application_state::images::ImageAvailability; use crate::coloring; use crate::coloring::{calculate_color_matrix, Coloring, DrawingOptions}; use crate::common::camera; @@ -285,9 +287,8 @@ impl ImageRenderer { if let Some(cv) = &image_view_data.currently_viewing { let image_id = cv.id(); match rendering_context.texture_by_id(image_id) { - crate::application_state::images::ImageAvailability::NotAvailable - | crate::application_state::images::ImageAvailability::Pending(_) => {} - crate::application_state::images::ImageAvailability::Available(texture) => { + ImageAvailability::NotAvailable | ImageAvailability::Pending(_) => {} + ImageAvailability::Available(texture) => { // for batch, we need to check if the batch item is available let batch_index = if matches!(cv, CurrentlyViewing::BatchItem(_)) { let batch_index = rendering_context @@ -318,61 +319,105 @@ impl ImageRenderer { Ok(()) } - fn render_image( + fn render_overlays( rendering_context: &dyn RenderingContext, rendering_data: &mut RenderingData, - texture: Mrc, batch_item: Option, image_view_data: &ImageViewData, view_name: &ViewId, ) { - let texture = texture.borrow(); + let overlays = &image_view_data.overlays; + let overlay = overlays.first(); + if let Some(overlay) = overlay { + // Render the overlay + let texture = rendering_context.texture_by_id(overlay); + if let ImageAvailability::Available(texture) = texture { + // Render the texture + } + } + } - let gl = &rendering_data.gl; - let mut _program_name; + fn program_for_texture<'p>( + texture: &TextureImage, + programs: &'p Programs, + ) -> &'p ProgramBundle { let texture_info = &texture.info; - - let program = match (texture_info.data_ordering, texture_info.channels) { + match (texture_info.data_ordering, texture_info.channels) { (DataOrdering::HWC, _) | (DataOrdering::CHW, Channels::One) => { match texture_info.datatype { - Datatype::Uint8 | Datatype::Uint16 | Datatype::Uint32 => { - _program_name = "uint_image"; - &rendering_data.programs.uint_image - } - Datatype::Float32 => { - _program_name = "normalized_image"; - &rendering_data.programs.normalized_image - } - Datatype::Int8 | Datatype::Int16 | Datatype::Int32 => { - _program_name = "int_image"; - &rendering_data.programs.int_image - } - Datatype::Bool => { - _program_name = "uint_image"; - &rendering_data.programs.uint_image - } + Datatype::Uint8 | Datatype::Uint16 | Datatype::Uint32 => &programs.uint_image, + Datatype::Float32 => &programs.normalized_image, + Datatype::Int8 | Datatype::Int16 | Datatype::Int32 => &programs.int_image, + Datatype::Bool => &programs.uint_image, } } (DataOrdering::CHW, _) => match texture_info.datatype { Datatype::Uint8 | Datatype::Uint32 | Datatype::Uint16 => { - _program_name = "planar_uint_image"; - &rendering_data.programs.planar_uint_image - } - Datatype::Float32 => { - _program_name = "planar_normalized_image"; - &rendering_data.programs.planar_normalized_image - } - Datatype::Int8 | Datatype::Int16 | Datatype::Int32 => { - _program_name = "planar_int_image"; - &rendering_data.programs.planar_int_image - } - Datatype::Bool => { - _program_name = "planar_uint_image"; - &rendering_data.programs.planar_uint_image + &programs.planar_uint_image } + Datatype::Float32 => &programs.planar_normalized_image, + Datatype::Int8 | Datatype::Int16 | Datatype::Int32 => &programs.planar_int_image, + Datatype::Bool => &programs.planar_uint_image, }, - }; + } + } + + fn get_texture_uniforms( + texture: &'_ TextureImage, + batch_index: u32, + ) -> HashMap<&'static str, UniformValue<'_>> { + match texture.textures[&batch_index] { + TexturesGroup::HWC(ref texture) => { + HashMap::from([("u_texture", UniformValue::Texture(texture))]) + } + TexturesGroup::CHW_G { ref gray } => { + // This one is using the same method as regular HWC, because it's not really a planar texture + HashMap::from([("u_texture", UniformValue::Texture(gray))]) + } + TexturesGroup::CHW_GA { + ref gray, + ref alpha, + } => HashMap::from([ + ("u_image_type", UniformValue::Int(&3)), + ("u_texture_r", UniformValue::Texture(gray)), + ("u_texture_g", UniformValue::Texture(alpha)), + ]), + TexturesGroup::CHW_RGB { + ref red, + ref green, + ref blue, + } => HashMap::from([ + ("u_image_type", UniformValue::Int(&1)), + ("u_texture_r", UniformValue::Texture(red)), + ("u_texture_g", UniformValue::Texture(green)), + ("u_texture_b", UniformValue::Texture(blue)), + ]), + TexturesGroup::CHW_RGBA { + ref red, + ref green, + ref blue, + ref alpha, + } => HashMap::from([ + ("u_image_type", UniformValue::Int(&2)), + ("u_texture_r", UniformValue::Texture(red)), + ("u_texture_g", UniformValue::Texture(green)), + ("u_texture_b", UniformValue::Texture(blue)), + ("u_texture_a", UniformValue::Texture(alpha)), + ]), + } + } + + fn prepare_texture_uniforms<'a>( + rendering_context: &dyn RenderingContext, + rendering_data: &'a RenderingData, + texture: &'a TextureImage, + colormap_texture: Option<&'a web_sys::WebGlTexture>, + batch_item: Option, + image_view_data: &ImageViewData, + uniform_values: &mut HashMap<&'static str, UniformValue<'a>>, + ) { + let texture_info = &texture.info; let config = rendering_context.rendering_configuration(); let html_element_size = Size { @@ -404,70 +449,179 @@ impl ImageRenderer { let coloring_factors = calculate_color_matrix(texture_info, &texture.computed_info, &drawing_options); - let mut uniform_values = HashMap::new(); - uniform_values.extend(HashMap::from([ - ("u_projectionMatrix", UniformValue::Mat3(&view_projection)), - ("u_enable_borders", UniformValue::Bool(&enable_borders)), - ("u_buffer_dimension", UniformValue::Vec2(&image_size_vec)), + ("u_projectionMatrix", UniformValue::Mat3_(view_projection)), + ("u_enable_borders", UniformValue::Bool_(enable_borders)), + ("u_buffer_dimension", UniformValue::Vec2_(image_size_vec)), ( "u_normalization_factor", - UniformValue::Float(&coloring_factors.normalization_factor), + UniformValue::Float_(coloring_factors.normalization_factor), ), ( "u_color_multiplier", - UniformValue::Mat4(&coloring_factors.color_multiplier), + UniformValue::Mat4_(coloring_factors.color_multiplier), ), ( "u_color_addition", - UniformValue::Vec4(&coloring_factors.color_addition), + UniformValue::Vec4_(coloring_factors.color_addition), ), - ("u_invert", UniformValue::Bool(&drawing_options.invert)), + ("u_invert", UniformValue::Bool_(drawing_options.invert)), ])); - let get_textures = |batch_index: u32| match texture.textures[&batch_index] { - TexturesGroup::HWC(ref texture) => { - HashMap::from([("u_texture", UniformValue::Texture(texture))]) + let is_batched = batch_item.is_some(); + let batch_index = batch_item.unwrap_or(0); + uniform_values.extend(ImageRenderer::get_texture_uniforms(texture, batch_index)); + + if let Some(colormap_texture) = colormap_texture { + uniform_values.insert("u_colormap", UniformValue::Texture(colormap_texture)); + uniform_values.insert("u_use_colormap", UniformValue::Bool(&true)); + } else { + uniform_values.insert("u_use_colormap", UniformValue::Bool(&false)); + uniform_values.insert( + "u_colormap", + UniformValue::Texture(&rendering_data.placeholder_texture), + ); + } + + // let texture_size = Vec2::new(texture.image_size().width, texture.image_size().height); + // uniform_values.insert("u_texture_size", UniformValue::Vec2(&texture_size)); + + if texture_info.channels == Channels::One { + if let Some(clip_min) = drawing_options.clip.min { + uniform_values.insert("u_clip_min", UniformValue::Bool(&true)); + uniform_values.insert("u_min_clip_value", UniformValue::Float_(clip_min)); + } else { + uniform_values.insert("u_clip_min", UniformValue::Bool(&false)); } - TexturesGroup::CHW_G { ref gray } => { - // This one is using the same method as regular HWC, because it's not really a planar texture - HashMap::from([("u_texture", UniformValue::Texture(gray))]) + if let Some(clip_max) = drawing_options.clip.max { + uniform_values.insert("u_clip_max", UniformValue::Bool(&true)); + uniform_values.insert("u_max_clip_value", UniformValue::Float_(clip_max)); + } else { + uniform_values.insert("u_clip_max", UniformValue::Bool(&false)); } - TexturesGroup::CHW_GA { - ref gray, - ref alpha, - } => HashMap::from([ - ("u_image_type", UniformValue::Int(&3)), - ("u_texture_r", UniformValue::Texture(gray)), - ("u_texture_g", UniformValue::Texture(alpha)), - ]), - TexturesGroup::CHW_RGB { - ref red, - ref green, - ref blue, - } => HashMap::from([ - ("u_image_type", UniformValue::Int(&1)), - ("u_texture_r", UniformValue::Texture(red)), - ("u_texture_g", UniformValue::Texture(green)), - ("u_texture_b", UniformValue::Texture(blue)), - ]), - TexturesGroup::CHW_RGBA { - ref red, - ref green, - ref blue, - ref alpha, - } => HashMap::from([ - ("u_image_type", UniformValue::Int(&2)), - ("u_texture_r", UniformValue::Texture(red)), - ("u_texture_g", UniformValue::Texture(green)), - ("u_texture_b", UniformValue::Texture(blue)), - ("u_texture_a", UniformValue::Texture(alpha)), - ]), + } + } + + #[allow(clippy::too_many_arguments)] + fn render_text( + rendering_context: &dyn RenderingContext, + rendering_data: &mut RenderingData, + texture: &TextureImage, + drawing_options: &DrawingOptions, + global_drawing_options: &GlobalDrawingOptions, + batch_item: Option, + image_view_data: &ImageViewData, + view_name: &ViewId, + ) { + let texture_info = &texture.info; + let html_element_size = Size { + width: image_view_data.html_element.client_width() as f32, + height: image_view_data.html_element.client_height() as f32, }; + let camera = &image_view_data.camera; + + let image_size = texture.image_size(); + let aspect_ratio = image_size.width / image_size.height; + + let view_projection = + camera::calculate_view_projection(&html_element_size, &VIEW_SIZE, camera, aspect_ratio); + + let pixels_info = + calculate_pixels_information(&image_size, &view_projection, &html_element_size); + + let coloring_factors = + calculate_color_matrix(texture_info, &texture.computed_info, drawing_options); let is_batched = batch_item.is_some(); let batch_index = batch_item.unwrap_or(0); - uniform_values.extend(get_textures(batch_index)); + + let pixel_text_cache = rendering_data + .pixel_text_cache_per_view + .get_mut(view_name) + .unwrap(); + + for x in pixels_info.lower_x_px..pixels_info.upper_x_px { + for y in pixels_info.lower_y_px..pixels_info.upper_y_px { + let image_pixels_to_view = Mat3::from_scale(Vec2::new( + VIEW_SIZE.width / texture.image_size().width, + VIEW_SIZE.height / texture.image_size().height, + )); + + let pixel = UVec2::new(x as _, y as _); + + let batch_index = if is_batched { batch_index } else { 0 }; + + let pixel_value = PixelValue::from_image_info( + &texture.info, + &texture.bytes[&batch_index], + &pixel, + ); + + // The actual pixel color might be different from the pixel value, depending on drawing options + let text_color = match drawing_options.coloring { + Coloring::Heatmap | Coloring::Segmentation => { + let name = match drawing_options.coloring { + Coloring::Heatmap => &global_drawing_options.heatmap_colormap_name, + Coloring::Segmentation => { + &global_drawing_options.segmentation_colormap_name + } + _ => unreachable!(), + }; + let colormap = rendering_context + .get_color_map(name) + .expect("Could not get color map"); + let pixel_color = coloring::calculate_pixel_color_from_colormap( + &pixel_value, + &coloring_factors, + colormap.as_ref(), + drawing_options, + ); + + text_color(pixel_color, &DrawingOptions::default()) + } + _ => { + let rgba = Vec4::from(pixel_value.as_rgba_f32()); + let pixel_color = coloring_factors.color_multiplier + * (rgba / coloring_factors.normalization_factor) + + coloring_factors.color_addition; + + text_color(pixel_color, drawing_options) + } + }; + + rendering_data.text_renderer.render(PixelTextRenderingData { + pixel_text_cache, + pixel_loc: &pixel, + pixel_value: &pixel_value, + image_coords_to_view_coord_mat: &image_pixels_to_view, + view_projection: &view_projection, + text_color: &text_color, + }); + } + } + } + + fn render_image( + rendering_context: &dyn RenderingContext, + rendering_data: &mut RenderingData, + texture: Mrc, + batch_item: Option, + image_view_data: &ImageViewData, + view_name: &ViewId, + ) { + let texture = texture.borrow(); + + let gl = &rendering_data.gl; + let program = ImageRenderer::program_for_texture(&texture, &rendering_data.programs); + let config = rendering_context.rendering_configuration(); + + let (drawing_options, global_drawing_options) = rendering_context.drawing_options( + image_view_data + .currently_viewing + .as_ref() + .map(CurrentlyViewing::id) + .unwrap(), + ); let colormap_texture = if Coloring::Heatmap == drawing_options.coloring { let color_map_texture = rendering_context @@ -487,121 +641,62 @@ impl ImageRenderer { None }; - let texture_size = Vec2::new(texture.image_size().width, texture.image_size().height); - uniform_values.insert("u_texture_size", UniformValue::Vec2(&texture_size)); - - if texture_info.channels == Channels::One { - if let Some(ref clip_min) = drawing_options.clip.min { - uniform_values.insert("u_clip_min", UniformValue::Bool(&true)); - uniform_values.insert("u_min_clip_value", UniformValue::Float(clip_min)); - } else { - uniform_values.insert("u_clip_min", UniformValue::Bool(&false)); - } - if let Some(ref clip_max) = drawing_options.clip.max { - uniform_values.insert("u_clip_max", UniformValue::Bool(&true)); - uniform_values.insert("u_max_clip_value", UniformValue::Float(clip_max)); - } else { - uniform_values.insert("u_clip_max", UniformValue::Bool(&false)); - } - } + let mut uniform_values = HashMap::new(); - if let Some(ref colormap_texture) = colormap_texture { - uniform_values.insert("u_colormap", UniformValue::Texture(colormap_texture)); - uniform_values.insert("u_use_colormap", UniformValue::Bool(&true)); - } else { - uniform_values.insert("u_use_colormap", UniformValue::Bool(&false)); - uniform_values.insert( - "u_colormap", - UniformValue::Texture(&rendering_data.placeholder_texture), - ); - } + ImageRenderer::prepare_texture_uniforms( + rendering_context, + rendering_data, + &texture, + colormap_texture.as_ref(), + batch_item, + image_view_data, + &mut uniform_values, + ); gl.use_program(Some(&program.program)); set_uniforms(program, &uniform_values); set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); - // let only_edges = true; - // uniform_values.insert("u_only_edges", UniformValue::Bool(&only_edges)); + // ImageRenderer::render_overlays( + // rendering_context, + // rendering_data, + // batch_item, + // image_view_data, + // view_name, + // ); + + let to_render_text = { + let html_element_size = Size { + width: image_view_data.html_element.client_width() as f32, + height: image_view_data.html_element.client_height() as f32, + }; + let camera = &image_view_data.camera; + let image_size = texture.image_size(); + let aspect_ratio = image_size.width / image_size.height; + let view_projection = camera::calculate_view_projection( + &html_element_size, + &VIEW_SIZE, + camera, + aspect_ratio, + ); + let pixels_info = + calculate_pixels_information(&image_size, &view_projection, &html_element_size); - // set_uniforms(program, &uniform_values); - // draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); + pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_values as _ + }; - let to_render_text = - pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_values as _; if to_render_text { - let pixel_text_cache = rendering_data - .pixel_text_cache_per_view - .get_mut(view_name) - .unwrap(); - - for x in pixels_info.lower_x_px..pixels_info.upper_x_px { - for y in pixels_info.lower_y_px..pixels_info.upper_y_px { - let image_pixels_to_view = Mat3::from_scale(Vec2::new( - VIEW_SIZE.width / texture.image_size().width, - VIEW_SIZE.height / texture.image_size().height, - )); - - let pixel = UVec2::new(x as _, y as _); - - let batch_index = if is_batched { batch_index } else { 0 }; - - let pixel_value = PixelValue::from_image_info( - &texture.info, - &texture.bytes[&batch_index], - &pixel, - ); - - // The actual pixel color might be different from the pixel value, depending on drawing options - let text_color = match drawing_options.coloring { - Coloring::Heatmap | Coloring::Segmentation => { - let name = match drawing_options.coloring { - Coloring::Heatmap => &global_drawing_options.heatmap_colormap_name, - Coloring::Segmentation => { - &global_drawing_options.segmentation_colormap_name - } - _ => unreachable!(), - }; - let colormap = rendering_context - .get_color_map(name) - .expect("Could not get color map"); - let pixel_color = coloring::calculate_pixel_color_from_colormap( - &pixel_value, - &coloring_factors, - colormap.as_ref(), - &drawing_options, - ); - - // if only_edges { - // pixel_color - // } else { - text_color(pixel_color, &DrawingOptions::default()) - // } - } - _ => { - let rgba = Vec4::from(pixel_value.as_rgba_f32()); - let pixel_color = coloring_factors.color_multiplier - * (rgba / coloring_factors.normalization_factor) - + coloring_factors.color_addition; - - // if only_edges { - // pixel_color - // } else { - text_color(pixel_color, &drawing_options) - // } - } - }; - - rendering_data.text_renderer.render(PixelTextRenderingData { - pixel_text_cache, - pixel_loc: &pixel, - pixel_value: &pixel_value, - image_coords_to_view_coord_mat: &image_pixels_to_view, - view_projection: &view_projection, - text_color: &text_color, - }); - } - } + ImageRenderer::render_text( + rendering_context, + rendering_data, + &texture, + &drawing_options, + &global_drawing_options, + batch_item, + image_view_data, + view_name, + ); } } } diff --git a/src/webview-ui/src/webgl_utils/program.rs b/src/webview-ui/src/webgl_utils/program.rs index 8a6ada16..4c7ec647 100644 --- a/src/webview-ui/src/webgl_utils/program.rs +++ b/src/webview-ui/src/webgl_utils/program.rs @@ -80,21 +80,37 @@ fn make_uniform_setter(gl_type: GLConstant, location: WebGlUniformLocation) -> U let _ = gl_type; Box::new(move |gl: &GL, value: &UniformValue| match value { UniformValue::Int(v) => gl.uniform1i(Some(&location), **v), + UniformValue::Int_(v) => gl.uniform1i(Some(&location), *v), UniformValue::Float(v) => gl.uniform1f(Some(&location), **v), + UniformValue::Float_(v) => gl.uniform1f(Some(&location), *v), UniformValue::Bool(v) => gl.uniform1i(Some(&location), **v as i32), + UniformValue::Bool_(v) => gl.uniform1i(Some(&location), *v as i32), UniformValue::Vec2(v) => gl.uniform2fv_with_f32_array(Some(&location), v.as_ref()), + UniformValue::Vec2_(v) => gl.uniform2fv_with_f32_array(Some(&location), v.as_ref()), UniformValue::Vec3(v) => gl.uniform3fv_with_f32_array(Some(&location), v.as_ref()), + UniformValue::Vec3_(v) => gl.uniform3fv_with_f32_array(Some(&location), v.as_ref()), UniformValue::Vec4(v) => gl.uniform4fv_with_f32_array(Some(&location), v.as_ref()), + UniformValue::Vec4_(v) => gl.uniform4fv_with_f32_array(Some(&location), v.as_ref()), UniformValue::Mat3(v) => gl.uniform_matrix3fv_with_f32_array( Some(&location), false, v.to_cols_array().as_slice(), ), + UniformValue::Mat3_(v) => gl.uniform_matrix3fv_with_f32_array( + Some(&location), + false, + v.to_cols_array().as_slice(), + ), UniformValue::Mat4(v) => gl.uniform_matrix4fv_with_f32_array( Some(&location), false, v.to_cols_array().as_slice(), ), + UniformValue::Mat4_(v) => gl.uniform_matrix4fv_with_f32_array( + Some(&location), + false, + v.to_cols_array().as_slice(), + ), UniformValue::Texture(_) => panic!("Texture should be handled separately"), }) } diff --git a/src/webview-ui/src/webgl_utils/types.rs b/src/webview-ui/src/webgl_utils/types.rs index 1b45cf0c..f43f22ad 100644 --- a/src/webview-ui/src/webgl_utils/types.rs +++ b/src/webview-ui/src/webgl_utils/types.rs @@ -19,18 +19,27 @@ pub(crate) type GLConstant = u32; pub(crate) trait GLValue: GLVerifyType + GLSet {} impl GLValue for T where T: GLVerifyType + GLSet {} +#[derive(Debug)] #[enum_dispatch(GLVerifyType, GLSet)] pub(crate) enum UniformValue<'a> { Int(&'a i32), + Int_(i32), Float(&'a f32), + Float_(f32), Bool(&'a bool), + Bool_(bool), Texture(&'a WebGlTexture), Vec2(&'a glam::Vec2), + Vec2_(glam::Vec2), Vec3(&'a glam::Vec3), + Vec3_(glam::Vec3), Vec4(&'a glam::Vec4), + Vec4_(glam::Vec4), Mat3(&'a glam::Mat3), + Mat3_(glam::Mat3), Mat4(&'a glam::Mat4), + Mat4_(glam::Mat4), } pub(crate) type UniformSetter = Box; From 548cc4cb9c008e8163ea4c82f8decc2c69a10f6f Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:03:47 +0200 Subject: [PATCH 05/18] wip --- src/webview-ui/debug_shaders/int-image.frag | 31 +- .../debug_shaders/int-planar-image.frag | 31 +- .../debug_shaders/normalized-image.frag | 31 +- .../normalized-planar-image.frag | 31 +- src/webview-ui/debug_shaders/uint-image.frag | 31 +- .../debug_shaders/uint-planar-image.frag | 31 +- src/webview-ui/shaders/src/shader_parts.rs | 31 +- src/webview-ui/src/app.rs | 11 +- .../src/application_state/app_state.rs | 57 +++- src/webview-ui/src/application_state/views.rs | 70 +++-- src/webview-ui/src/components/main_toolbar.rs | 267 +++++++++++++++++- .../src/rendering/image_renderer.rs | 136 +++++++-- .../src/rendering/rendering_context.rs | 4 +- 13 files changed, 664 insertions(+), 98 deletions(-) diff --git a/src/webview-ui/debug_shaders/int-image.frag b/src/webview-ui/debug_shaders/int-image.frag index 2358946e..36f05787 100644 --- a/src/webview-ui/debug_shaders/int-image.frag +++ b/src/webview-ui/debug_shaders/int-image.frag @@ -22,6 +22,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -119,11 +123,26 @@ sampled = } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -135,8 +154,12 @@ sampled = color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/debug_shaders/int-planar-image.frag b/src/webview-ui/debug_shaders/int-planar-image.frag index bb751035..19a03329 100644 --- a/src/webview-ui/debug_shaders/int-planar-image.frag +++ b/src/webview-ui/debug_shaders/int-planar-image.frag @@ -27,6 +27,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -136,11 +140,26 @@ if ((need & NEED_ALPHA) != 0) { } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -152,8 +171,12 @@ if ((need & NEED_ALPHA) != 0) { color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/debug_shaders/normalized-image.frag b/src/webview-ui/debug_shaders/normalized-image.frag index 63228a77..0e79cfe1 100644 --- a/src/webview-ui/debug_shaders/normalized-image.frag +++ b/src/webview-ui/debug_shaders/normalized-image.frag @@ -21,6 +21,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -116,11 +120,26 @@ sampled = texture(u_texture, pix); } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -132,8 +151,12 @@ sampled = texture(u_texture, pix); color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/debug_shaders/normalized-planar-image.frag b/src/webview-ui/debug_shaders/normalized-planar-image.frag index f42f248b..f8dfd83a 100644 --- a/src/webview-ui/debug_shaders/normalized-planar-image.frag +++ b/src/webview-ui/debug_shaders/normalized-planar-image.frag @@ -26,6 +26,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -135,11 +139,26 @@ void main() { } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -151,8 +170,12 @@ void main() { color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/debug_shaders/uint-image.frag b/src/webview-ui/debug_shaders/uint-image.frag index ca26b55e..ee9d40b9 100644 --- a/src/webview-ui/debug_shaders/uint-image.frag +++ b/src/webview-ui/debug_shaders/uint-image.frag @@ -22,6 +22,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -119,11 +123,26 @@ sampled = } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -135,8 +154,12 @@ sampled = color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/debug_shaders/uint-planar-image.frag b/src/webview-ui/debug_shaders/uint-planar-image.frag index c3a8d6ca..7f44c7d3 100644 --- a/src/webview-ui/debug_shaders/uint-planar-image.frag +++ b/src/webview-ui/debug_shaders/uint-planar-image.frag @@ -27,6 +27,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -136,11 +140,26 @@ if ((need & NEED_ALPHA) != 0) { } } - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) { + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + } vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) { + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) { + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) { + discard; + } + } + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -152,8 +171,12 @@ if ((need & NEED_ALPHA) != 0) { color.rgb += vec3(vertical_border + horizontal_border); } - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) { + color.a = u_overlay_alpha; + } else { + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + } fout_color = color; } diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs index 4bc0714c..8efefec8 100644 --- a/src/webview-ui/shaders/src/shader_parts.rs +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -28,6 +28,10 @@ uniform bool u_clip_max; uniform float u_min_clip_value; uniform float u_max_clip_value; +// overlay related uniforms +uniform bool u_is_overlay; +uniform float u_overlay_alpha; + uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -104,11 +108,26 @@ void main() {{ }} }} - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + if (!u_is_overlay) {{ + float c = checkboard(gl_FragCoord.xy); + color.rgb = mix(vec3(c, c, c), color.rgb, color.a); + }} vec2 buffer_position = vout_uv * u_buffer_dimension; if (u_enable_borders) {{ + // in case of overlay, we discard this fragment for pixels that are on the border + if (u_is_overlay) {{ + bool is_border = ( + buffer_position.x < 1.0 || + buffer_position.x > u_buffer_dimension.x - 1.0 || + buffer_position.y < 1.0 || + buffer_position.y > u_buffer_dimension.y - 1.0 + ); + if (is_border) {{ + discard; + }} + }} + float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); @@ -120,8 +139,12 @@ void main() {{ color.rgb += vec3(vertical_border + horizontal_border); }} - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; + if (u_is_overlay) {{ + color.a = u_overlay_alpha; + }} else {{ + // alpha is always 1.0 after checkboard is mixed in + color.a = 1.0; + }} fout_color = color; }} diff --git a/src/webview-ui/src/app.rs b/src/webview-ui/src/app.rs index 4046ef70..05a0c3a2 100644 --- a/src/webview-ui/src/app.rs +++ b/src/webview-ui/src/app.rs @@ -70,9 +70,14 @@ fn rendering_context() -> impl RenderingContext { let dispatch = Dispatch::::global(); let state = dispatch.get(); let currently_viewing = state.image_views.borrow().get_currently_viewing(view_id); - let overlays = currently_viewing.as_ref().map_or(Vec::new(), |cv| { - state.overlays.borrow().get_overlays(view_id, cv.id()) + let overlay = currently_viewing.as_ref().and_then(|cv| { + state + .overlays + .borrow() + .get_image_overlay(view_id, cv.id()) + .cloned() }); + let camera = state.view_cameras.borrow().get(view_id); let html_element = state .image_views @@ -90,7 +95,7 @@ fn rendering_context() -> impl RenderingContext { camera, html_element, currently_viewing, - overlays, + overlay, } } diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index 03ce8eba..9da0eb80 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -514,6 +514,19 @@ pub(crate) enum OverlayAction { image_id: ViewableObjectId, overlay_id: ViewableObjectId, }, + Hide { + view_id: ViewId, + image_id: ViewableObjectId, + }, + Show { + view_id: ViewId, + image_id: ViewableObjectId, + }, + SetAlpha { + view_id: ViewId, + image_id: ViewableObjectId, + alpha: f32, + }, } impl Reducer for OverlayAction { @@ -529,7 +542,49 @@ impl Reducer for OverlayAction { state .overlays .borrow_mut() - .add_overlay(view_id, image_id, overlay_id); + .add_overlay_to_image(view_id, image_id, overlay_id); + } + OverlayAction::Hide { + view_id, + image_id: overlay_id, + } => { + if let Some(overlay_item) = state + .overlays + .borrow_mut() + .get_image_overlay_mut(view_id, &overlay_id) + { + overlay_item.hidden = true; + } + } + OverlayAction::Show { view_id, image_id } => { + if let Some(overlay_item) = state + .overlays + .borrow_mut() + .get_image_overlay_mut(view_id, &image_id) + { + overlay_item.hidden = false; + } + } + OverlayAction::SetAlpha { + view_id, + image_id, + alpha, + } => { + if let Some(overlay_item) = state + .overlays + .borrow_mut() + .get_image_overlay_mut(view_id, &image_id) + { + let mut alpha = alpha; + // Clamp alpha to [0.0, 1.0] range, with a threshold to avoid flickering + if alpha < 0.02 { + alpha = 0.0; + } + if alpha > 0.98 { + alpha = 1.0; + } + overlay_item.alpha = alpha; + } } } diff --git a/src/webview-ui/src/application_state/views.rs b/src/webview-ui/src/application_state/views.rs index d16340c1..e1229d8b 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -1,12 +1,12 @@ -use std::{ - collections::{HashMap, HashSet}, - iter::FromIterator, -}; +use std::{collections::HashMap, iter::FromIterator}; use web_sys::{CustomEvent, HtmlElement}; use yew::NodeRef; -use crate::common::{constants::all_views, CurrentlyViewing, ViewId, ViewableObjectId}; +use crate::{ + coloring::DrawingOptions, + common::{constants::all_views, CurrentlyViewing, ViewId, ViewableObjectId}, +}; pub(crate) struct ImageViews(HashMap, NodeRef)>); @@ -90,22 +90,45 @@ impl Default for ImageViews { } } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct OverlayItem { + pub(crate) view_id: ViewId, + pub(crate) id: ViewableObjectId, + pub(crate) hidden: bool, + pub(crate) alpha: f32, + pub(crate) only_edges: bool, + pub(crate) display_options: DrawingOptions, +} + +impl OverlayItem { + fn new(view_id: ViewId, id: ViewableObjectId) -> Self { + Self { + view_id, + id, + hidden: false, + alpha: 0.4, + only_edges: false, + display_options: DrawingOptions::default(), + } + } +} + #[derive(Debug, Default)] pub(crate) struct Overlays { - overlays: HashMap<(ViewId, ViewableObjectId), HashSet>, + overlays: HashMap<(ViewId, ViewableObjectId), OverlayItem>, } impl Overlays { - pub(crate) fn add_overlay( + pub(crate) fn add_overlay_to_image( &mut self, view_id: ViewId, image_id: ViewableObjectId, overlay_id: ViewableObjectId, ) { - self.overlays - .entry((view_id, image_id)) - .or_default() - .insert(overlay_id); + self.overlays.insert( + (view_id, image_id.clone()), + OverlayItem::new(view_id, overlay_id), + ); } pub(crate) fn remove_overlay( @@ -114,22 +137,27 @@ impl Overlays { image_id: &ViewableObjectId, overlay_id: &ViewableObjectId, ) { - if let Some(overlays) = self.overlays.get_mut(&(view_id, image_id.clone())) { - overlays.retain(|id| id != overlay_id); + if let Some(overlay_item) = self.overlays.get_mut(&(view_id, image_id.clone())) { + if overlay_item.id == *overlay_id { + self.overlays.remove(&(view_id, image_id.clone())); + } } } - pub(crate) fn get_overlays( + pub(crate) fn get_image_overlay( &self, view_id: ViewId, image_id: &ViewableObjectId, - ) -> Vec { - self.overlays - .get(&(view_id, image_id.clone())) - .cloned() - .unwrap_or_default() - .into_iter() - .collect() + ) -> Option<&OverlayItem> { + self.overlays.get(&(view_id, image_id.clone())) + } + + pub(crate) fn get_image_overlay_mut( + &mut self, + view_id: ViewId, + image_id: &ViewableObjectId, + ) -> Option<&mut OverlayItem> { + self.overlays.get_mut(&(view_id, image_id.clone())) } pub(crate) fn clear_overlays(&mut self, view_id: ViewId, image_id: &ViewableObjectId) { diff --git a/src/webview-ui/src/components/main_toolbar.rs b/src/webview-ui/src/components/main_toolbar.rs index 9a389f24..140f4ed6 100644 --- a/src/webview-ui/src/components/main_toolbar.rs +++ b/src/webview-ui/src/components/main_toolbar.rs @@ -6,14 +6,20 @@ use stylist::{ use wasm_bindgen::JsCast; use yew::prelude::*; -use yewdux::{prelude::use_selector, use_selector_with_deps, Dispatch}; +use yewdux::{dispatch, prelude::use_selector, use_selector_with_deps, Dispatch}; use crate::{ - application_state::app_state::{AppState, StoreAction, UpdateGlobalDrawingOptions}, + application_state::{ + app_state::{AppState, OverlayAction, StoreAction, UpdateGlobalDrawingOptions}, + views::OverlayItem, + }, coloring::Coloring, colormap::ColorMapKind, - common::{AppMode, Image, ViewId}, - components::{checkbox::Checkbox, display_options::DisplayOption, icon_button::IconButton}, + common::{AppMode, CurrentlyViewing, Image, ViewId}, + components::{ + checkbox::Checkbox, display_options::DisplayOption, icon_button::IconButton, + icon_button::IconButton, + }, vscode::vscode_requests::VSCodeRequests, }; @@ -109,6 +115,214 @@ pub fn HeatmapColormapDropdown(props: &HeatmapColormapDropdownProps) -> Html { } } +#[derive(PartialEq, Properties)] +pub struct OverlayMenuItemProps { + overlay: OverlayItem, +} + +#[function_component] +pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { + let OverlayMenuItemProps { overlay } = props; + let overlay = overlay.clone(); + + let info = use_selector_with_deps( + |state: &AppState, overlay: &OverlayItem| { + let images = state.images.borrow(); + let image = images + .get(&overlay.id) + .unwrap_or_else(|| panic!("Image with id {:?} not found", overlay.id)); + if let Image::Full(info) = image { + info.clone() + } else { + panic!("Overlay item is not a full image: {:?}", overlay.id); + } + }, + overlay.clone(), + ); + + let cv = use_selector(move |state: &AppState| { + state + .image_views + .borrow() + .get_currently_viewing(overlay.view_id) + }); + let cv_image_id = cv.as_ref().as_ref().map(|cv| cv.id().clone()); + if cv_image_id.is_none() { + log::warn!( + "OverlayMenuItem: No currently viewing image found for view_id {:?}", + overlay.view_id + ); + return html! {}; + } + let cv_image_id = cv_image_id.unwrap(); + let view_id = overlay.view_id; + + let expression = use_selector_with_deps( + |state: &AppState, overlay: &OverlayItem| { + let images = state.images.borrow(); + let info = images + .get(&overlay.id) + .unwrap_or_else(|| panic!("Image with id {:?} not found", overlay.id)) + .minimal(); + info.expression.clone() + }, + overlay.clone(), + ); + + let dispatch = Dispatch::::global(); + let hide_button = html! { + + }; + let show_button = html! { + + }; + let show_hide_button = if overlay.hidden { + show_button + } else { + hide_button + }; + + let alpha_state = use_state(|| 1.0); + use_effect_with(overlay.alpha, { + let alpha_state = alpha_state.clone(); + move |alpha| { + alpha_state.set(*alpha); + || () + } + }); + let alpha_throttle = { + let alpha_state = alpha_state.clone(); + let cv_image_id = cv_image_id.clone(); + yew_hooks::use_throttle( + move || { + dispatch.apply(OverlayAction::SetAlpha { + view_id: overlay.view_id, + image_id: cv_image_id.clone(), + alpha: *alpha_state, + }); + }, + 10, + ) + }; + let alpha_slider = html! { + () + .unwrap() + .value(); + if let Ok(value) = value.parse::() { + alpha_state.set(value); + alpha_throttle.run(); + } + } + }) + } + /> + }; + + let display_options = html! { + + }; + + let style = use_style!( + r#" + position: relative; + + .top { + display: flex; + align-items: center; + justify-content: flex-start; + flex-direction: row; + gap: 10px; + } + + .overlay-id { + font-size: 0.9em; + color: var(--vscode-foreground); + } + + .controls-container { + z-index: 10; + position: absolute; + bottom: 0; + left: 0; + right: 0; + transform: translateY(100%); + background-color: var(--vscode-sideBar-background); + border: 1px solid var(--vscode-panel-border); + padding: 5px; + } + + .slider { + width: 100px; + margin-left: 10px; + } + "# + ); + + html! { +
+
+ + {show_hide_button} + + + {expression} + +
+
+
+ { display_options } +
+
+ + {"Alpha"} + + { alpha_slider } +
+
+
+ } +} + #[derive(PartialEq, Properties)] pub(crate) struct MainToolbarProps {} @@ -147,6 +361,32 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { (cv.clone(), app_mode.clone()), ); + // Colorbar visibility + let display_colorbar = + use_selector(|state: &AppState| state.global_drawing_options.display_colorbar); + let dispatch = Dispatch::::global(); + let on_colorbar_change = Callback::from(move |checked: bool| { + dispatch.apply(StoreAction::UpdateGlobalDrawingOptions( + UpdateGlobalDrawingOptions::DisplayColorbar(checked), + )); + }); + + // Overlay related + let overlay = use_selector_with_deps( + { + move |state: &AppState, cv: &Option| { + cv.as_ref().and_then(|cv| { + state + .overlays + .borrow() + .get_image_overlay(ViewId::Primary, cv.id()) + .cloned() + }) + } + }, + (*cv).clone(), + ); + let style = use_style!( r#" box-sizing: border-box; @@ -201,15 +441,6 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { "# ); - let display_colorbar = - use_selector(|state: &AppState| state.global_drawing_options.display_colorbar); - let dispatch = Dispatch::::global(); - let on_colorbar_change = Callback::from(move |checked: bool| { - dispatch.apply(StoreAction::UpdateGlobalDrawingOptions( - UpdateGlobalDrawingOptions::DisplayColorbar(checked), - )); - }); - // Get image info for save button let current_image_info = use_selector_with_deps( |state: &AppState, cv| { @@ -251,8 +482,11 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { > {"Colorbar"} +
+ +
Html { title={Some(AttrValue::from("Save Image"))} disabled={Some(!has_image)} /> + + if let Some(overlay) = overlay.as_ref() { +
+ +
+ } +

{"Click + Drag to pan"}

diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index 49f49272..7190dbce 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -11,6 +11,7 @@ use web_sys::{WebGl2RenderingContext as GL, WebGl2RenderingContext}; use crate::application_state::app_state::GlobalDrawingOptions; use crate::application_state::images::ImageAvailability; +use crate::application_state::views::OverlayItem; use crate::coloring; use crate::coloring::{calculate_color_matrix, Coloring, DrawingOptions}; use crate::common::camera; @@ -171,11 +172,11 @@ impl ImageRenderer { let gl = rendering_context.gl().clone(); gl.enable(WebGl2RenderingContext::SCISSOR_TEST); - gl.enable(WebGl2RenderingContext::BLEND); - gl.blend_func( - WebGl2RenderingContext::SRC_ALPHA, - WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA, - ); + + gl.enable(GL::BLEND); + gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA); + + gl.enable(GL::DEPTH_TEST); gl.depth_mask(false); let programs = ImageRenderer::create_programs(&gl).unwrap(); @@ -319,24 +320,6 @@ impl ImageRenderer { Ok(()) } - fn render_overlays( - rendering_context: &dyn RenderingContext, - rendering_data: &mut RenderingData, - batch_item: Option, - image_view_data: &ImageViewData, - view_name: &ViewId, - ) { - let overlays = &image_view_data.overlays; - let overlay = overlays.first(); - if let Some(overlay) = overlay { - // Render the overlay - let texture = rendering_context.texture_by_id(overlay); - if let ImageAvailability::Available(texture) = texture { - // Render the texture - } - } - } - fn program_for_texture<'p>( texture: &TextureImage, programs: &'p Programs, @@ -601,6 +584,95 @@ impl ImageRenderer { } } + fn render_overlay( + rendering_context: &dyn RenderingContext, + rendering_data: &mut RenderingData, + texture: &TextureImage, + overlay_item: &OverlayItem, + batch_item: Option, + image_view_data: &ImageViewData, + view_name: &ViewId, + ) { + let gl = &rendering_data.gl; + let program = ImageRenderer::program_for_texture(texture, &rendering_data.programs); + + let config = rendering_context.rendering_configuration(); + + let (drawing_options, global_drawing_options) = rendering_context.drawing_options( + image_view_data + .currently_viewing + .as_ref() + .map(CurrentlyViewing::id) + .unwrap(), + ); + + let colormap_texture = if Coloring::Heatmap == drawing_options.coloring { + let color_map_texture = rendering_context + .get_color_map_texture(&global_drawing_options.heatmap_colormap_name) + .expect("Could not get color map texture"); + + Some(color_map_texture.obj.clone()) + } else if Coloring::Segmentation == drawing_options.coloring { + let color_map_texture = rendering_context + .get_color_map_texture(&global_drawing_options.segmentation_colormap_name) + .expect("Could not get color map texture"); + + Some(color_map_texture.obj.clone()) + } else { + None + }; + + let mut uniform_values = HashMap::new(); + + ImageRenderer::prepare_texture_uniforms( + rendering_context, + rendering_data, + texture, + colormap_texture.as_ref(), + batch_item, + image_view_data, + &mut uniform_values, + ); + + // Overlay specific uniforms + uniform_values.insert("u_is_overlay", UniformValue::Bool(&true)); + uniform_values.insert("u_overlay_alpha", UniformValue::Float(&overlay_item.alpha)); + + gl.use_program(Some(&program.program)); + set_uniforms(program, &uniform_values); + set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); + draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); + } + + fn render_overlays( + rendering_context: &dyn RenderingContext, + rendering_data: &mut RenderingData, + batch_item: Option, + image_view_data: &ImageViewData, + view_name: &ViewId, + ) { + if let Some(overlay) = &image_view_data + .overlay + .as_ref() + .and_then(|o| (!o.hidden && o.alpha > 0.0).then_some(o)) + { + let texture = rendering_context.texture_by_id(&overlay.id); + // log::debug!("Rendering overlay {:?}", overlay); + if let ImageAvailability::Available(texture) = texture { + let texture = texture.borrow(); + ImageRenderer::render_overlay( + rendering_context, + rendering_data, + &texture, + overlay, + batch_item, + image_view_data, + view_name, + ); + } + } + } + fn render_image( rendering_context: &dyn RenderingContext, rendering_data: &mut RenderingData, @@ -653,18 +725,22 @@ impl ImageRenderer { &mut uniform_values, ); + // Set the overlay specific uniforms + uniform_values.insert("u_is_overlay", UniformValue::Bool(&false)); + uniform_values.insert("u_overlay_alpha", UniformValue::Float(&0.0)); + gl.use_program(Some(&program.program)); set_uniforms(program, &uniform_values); set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); draw_buffer_info(gl, &rendering_data.image_plane_buffer, DrawMode::Triangles); - // ImageRenderer::render_overlays( - // rendering_context, - // rendering_data, - // batch_item, - // image_view_data, - // view_name, - // ); + ImageRenderer::render_overlays( + rendering_context, + rendering_data, + batch_item, + image_view_data, + view_name, + ); let to_render_text = { let html_element_size = Size { diff --git a/src/webview-ui/src/rendering/rendering_context.rs b/src/webview-ui/src/rendering/rendering_context.rs index b2c284d4..fef1b38a 100644 --- a/src/webview-ui/src/rendering/rendering_context.rs +++ b/src/webview-ui/src/rendering/rendering_context.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use web_sys::{HtmlElement, WebGl2RenderingContext}; use crate::{ - application_state::{app_state::GlobalDrawingOptions, images::ImageAvailability}, + application_state::{app_state::GlobalDrawingOptions, images::ImageAvailability, views::OverlayItem}, coloring::DrawingOptions, colormap, common::{ @@ -18,7 +18,7 @@ use crate::{ pub(crate) struct ImageViewData { pub html_element: HtmlElement, pub currently_viewing: Option, - pub overlays: Vec, + pub overlay: Option, pub camera: camera::Camera, } From 3026be95def0f3285e62acf1fdf2fd26da4a4794 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:03:48 +0200 Subject: [PATCH 06/18] wip --- src/webview-ui/shaders/src/shader_parts.rs | 7 +- src/webview-ui/src/app.rs | 15 ++- .../src/application_state/app_state.rs | 49 ++++++--- .../src/application_state/images.rs | 47 +++++--- src/webview-ui/src/application_state/views.rs | 22 +--- .../application_state/vscode_data_fetcher.rs | 12 +-- src/webview-ui/src/coloring.rs | 2 + src/webview-ui/src/components/colorbar.rs | 7 +- .../src/components/display_options.rs | 101 +++++++----------- .../src/components/image_list_item.rs | 4 +- .../src/components/image_selection_list.rs | 4 +- src/webview-ui/src/components/main.rs | 4 +- src/webview-ui/src/components/main_toolbar.rs | 65 +++++++---- .../src/components/view_container.rs | 37 ++++--- .../src/rendering/image_renderer.rs | 55 ++++++---- .../src/rendering/rendering_context.rs | 9 +- 16 files changed, 254 insertions(+), 186 deletions(-) diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs index 8efefec8..a2fc7d66 100644 --- a/src/webview-ui/shaders/src/shader_parts.rs +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -31,6 +31,7 @@ uniform float u_max_clip_value; // overlay related uniforms uniform bool u_is_overlay; uniform float u_overlay_alpha; +uniform bool u_zeros_as_transparent; uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -108,6 +109,10 @@ void main() {{ }} }} + if (u_zeros_as_transparent && sampled.r == 0. && sampled.g == 0. && sampled.b == 0.) {{ + color.a = 0.0; + }} + if (!u_is_overlay) {{ float c = checkboard(gl_FragCoord.xy); color.rgb = mix(vec3(c, c, c), color.rgb, color.a); @@ -140,7 +145,7 @@ void main() {{ }} if (u_is_overlay) {{ - color.a = u_overlay_alpha; + color.a = u_overlay_alpha * color.a; }} else {{ // alpha is always 1.0 after checkboard is mixed in color.a = 1.0; diff --git a/src/webview-ui/src/app.rs b/src/webview-ui/src/app.rs index 05a0c3a2..c3584108 100644 --- a/src/webview-ui/src/app.rs +++ b/src/webview-ui/src/app.rs @@ -4,6 +4,7 @@ use crate::application_state::app_state::GlobalDrawingOptions; use crate::application_state::app_state::ImageObject; use crate::application_state::app_state::StoreAction; use crate::application_state::app_state::UpdateDrawingOptions; +use crate::application_state::images::DrawingContext; use crate::application_state::images::ImageAvailability; use crate::coloring::Coloring; use crate::coloring::DrawingOptions; @@ -107,10 +108,16 @@ fn rendering_context() -> impl RenderingContext { fn drawing_options( &self, image_id: &ViewableObjectId, + drawing_context: &DrawingContext, ) -> (DrawingOptions, GlobalDrawingOptions) { let dispatch = Dispatch::::global(); let state = dispatch.get(); - let drawing_options = state.drawing_options.borrow().get_or_default(image_id); + let drawing_options = state + .drawing_options + .borrow() + .get(image_id, drawing_context) + .cloned() + .unwrap_or_default(); let global_drawing_options = state.global_drawing_options.clone(); (drawing_options, global_drawing_options) } @@ -152,8 +159,9 @@ fn rendering_context() -> impl RenderingContext { let drawing_options = state .drawing_options .borrow() - .get(cv.id()) - .take_if(|drawing_options| drawing_options.coloring == Coloring::Heatmap); + .get(cv.id(), &DrawingContext::BaseImage) + .take_if(|drawing_options| drawing_options.coloring == Coloring::Heatmap) + .cloned(); let global_drawing_options = state.global_drawing_options.clone(); if let ImageAvailability::Available(texture_image) = self.texture_by_id(cv.id()) { html_element @@ -285,6 +293,7 @@ pub(crate) fn App() -> Html { ))); dispatch.apply(StoreAction::UpdateDrawingOptions( id.clone(), + DrawingContext::BaseImage, UpdateDrawingOptions::Full(drawing_options.clone()), )); dispatch.apply(StoreAction::SetImageToView(id.clone(), view_id)); diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index 9da0eb80..beee6a3c 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -3,6 +3,7 @@ use super::images::{ImageCache, Images, ImagesDrawingOptions}; use super::sessions::Sessions; use super::views::ImageViews; use super::vscode_data_fetcher::ImagesFetcher; +use crate::application_state::images::DrawingContext; use crate::application_state::views::Overlays; use crate::coloring::{Clip, Coloring, DrawingOptions}; use crate::common::camera::ViewsCameras; @@ -130,6 +131,7 @@ impl AppState { } } +#[derive(PartialEq, Clone)] pub(crate) enum UpdateDrawingOptions { Full(DrawingOptions), Reset, @@ -166,7 +168,7 @@ pub(crate) enum StoreAction { SetActiveSession(SessionId), SetImageToView(ViewableObjectId, ViewId), AddImageWithData(ViewableObjectId, ImageData), - UpdateDrawingOptions(ViewableObjectId, UpdateDrawingOptions), + UpdateDrawingOptions(ViewableObjectId, DrawingContext, UpdateDrawingOptions), UpdateGlobalDrawingOptions(UpdateGlobalDrawingOptions), ReplaceData(Vec), UpdateData(ImageObject), @@ -229,7 +231,7 @@ fn handle_received_image(state: &AppState, image: ImageObject) -> Result<()> { state .drawing_options .borrow_mut() - .mut_ref_or_default(image_id.clone()) + .get_mut_ref(image_id.clone(), DrawingContext::BaseImage) .batch_item .get_or_insert(0); } @@ -259,11 +261,15 @@ impl Reducer for StoreAction { match self { StoreAction::SetImageToView(image_id, view_id) => { - let drawing_options = state.drawing_options.borrow().get_or_default(&image_id); + let drawing_options = state + .drawing_options + .borrow() + .get(&image_id, &DrawingContext::BaseImage) + .cloned(); VSCodeRequests::update_state( HostExtensionStateUpdate::default() .current_image_id(Some(image_id.clone())) - .current_image_drawing_options(Some(drawing_options.clone())) + .current_image_drawing_options(drawing_options) .clone(), ); state.sessions.borrow_mut().active_session = Some(image_id.session_id().clone()); @@ -278,20 +284,29 @@ impl Reducer for StoreAction { }) .ok(); } - StoreAction::UpdateDrawingOptions(image_id, update) => { + StoreAction::UpdateDrawingOptions(image_id, drawing_context, update) => { let current_drawing_options = state .drawing_options .borrow() - .get_or_default(&image_id) - .clone(); + .get(&image_id, &drawing_context) + .cloned() + .unwrap_or_default(); let new_drawing_option = match update { UpdateDrawingOptions::Full(drawing_options) => drawing_options, - UpdateDrawingOptions::Reset => DrawingOptions { // keep the batch slice index batch_item: current_drawing_options.batch_item, ..DrawingOptions::default() }, + UpdateDrawingOptions::Coloring(Coloring::Segmentation) => DrawingOptions { + coloring: Coloring::Segmentation, + zeros_as_transparent: if drawing_context == DrawingContext::BaseImage { + current_drawing_options.zeros_as_transparent + } else { + true + }, + ..current_drawing_options + }, UpdateDrawingOptions::Coloring(c) => DrawingOptions { coloring: c, ..current_drawing_options @@ -328,10 +343,11 @@ impl Reducer for StoreAction { .current_image_drawing_options(Some(new_drawing_option.clone())) .clone(), ); - state - .drawing_options - .borrow_mut() - .set(image_id, new_drawing_option); + state.drawing_options.borrow_mut().set( + image_id, + drawing_context, + new_drawing_option, + ); } StoreAction::ReplaceData(replacement_images) => { log::debug!("ReplaceData"); @@ -474,7 +490,12 @@ impl Reducer for UiAction { UiAction::ViewShiftScroll(view_id, cv, amount) => { let id = cv.id(); - let current_drawing_options = state.drawing_options.borrow().get_or_default(id); + let current_drawing_options = state + .drawing_options + .borrow() + .get(id, &DrawingContext::BaseImage) + .cloned() + .unwrap_or_default(); if let (Some(current_index), Some(Image::Full(info))) = ( current_drawing_options.batch_item, state.images.borrow().get(id), @@ -488,7 +509,7 @@ impl Reducer for UiAction { state .drawing_options .borrow_mut() - .mut_ref_or_default(id.clone()) + .get_mut_ref(id.clone(), DrawingContext::BaseImage) .batch_item = Some(new_index); // send event to view that the batch item has changed diff --git a/src/webview-ui/src/application_state/images.rs b/src/webview-ui/src/application_state/images.rs index 61349d28..58801444 100644 --- a/src/webview-ui/src/application_state/images.rs +++ b/src/webview-ui/src/application_state/images.rs @@ -172,26 +172,49 @@ impl Images { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum DrawingContext { + BaseImage, + Overlay, +} + #[derive(Default)] -pub(crate) struct ImagesDrawingOptions(HashMap); +pub(crate) struct ImagesDrawingOptions( + HashMap>, +); impl ImagesDrawingOptions { - pub(crate) fn set(&mut self, image_id: ViewableObjectId, drawing_options: DrawingOptions) { - self.0.insert(image_id, drawing_options); + pub(crate) fn set( + &mut self, + image_id: ViewableObjectId, + drawing_context: DrawingContext, + drawing_options: DrawingOptions, + ) { + self.0 + .entry(image_id) + .or_default() + .insert(drawing_context, drawing_options); } - pub(crate) fn get_or_default(&self, image_id: &ViewableObjectId) -> DrawingOptions { + pub(crate) fn get( + &self, + image_id: &ViewableObjectId, + drawing_context: &DrawingContext, + ) -> Option<&DrawingOptions> { self.0 .get(image_id) - .cloned() - .unwrap_or(DrawingOptions::default()) - } - - pub(crate) fn get(&self, image_id: &ViewableObjectId) -> Option { - self.0.get(image_id).cloned() + .and_then(|contexts| contexts.get(drawing_context)) } - pub(crate) fn mut_ref_or_default(&mut self, image_id: ViewableObjectId) -> &mut DrawingOptions { - self.0.entry(image_id).or_default() + pub(crate) fn get_mut_ref( + &mut self, + image_id: ViewableObjectId, + drawing_context: DrawingContext, + ) -> &mut DrawingOptions { + self.0 + .entry(image_id) + .or_default() + .entry(drawing_context) + .or_default() } } diff --git a/src/webview-ui/src/application_state/views.rs b/src/webview-ui/src/application_state/views.rs index e1229d8b..997971e6 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -3,10 +3,7 @@ use std::{collections::HashMap, iter::FromIterator}; use web_sys::{CustomEvent, HtmlElement}; use yew::NodeRef; -use crate::{ - coloring::DrawingOptions, - common::{constants::all_views, CurrentlyViewing, ViewId, ViewableObjectId}, -}; +use crate::common::{constants::all_views, CurrentlyViewing, ViewId, ViewableObjectId}; pub(crate) struct ImageViews(HashMap, NodeRef)>); @@ -97,7 +94,6 @@ pub(crate) struct OverlayItem { pub(crate) hidden: bool, pub(crate) alpha: f32, pub(crate) only_edges: bool, - pub(crate) display_options: DrawingOptions, } impl OverlayItem { @@ -108,7 +104,6 @@ impl OverlayItem { hidden: false, alpha: 0.4, only_edges: false, - display_options: DrawingOptions::default(), } } } @@ -131,19 +126,6 @@ impl Overlays { ); } - pub(crate) fn remove_overlay( - &mut self, - view_id: ViewId, - image_id: &ViewableObjectId, - overlay_id: &ViewableObjectId, - ) { - if let Some(overlay_item) = self.overlays.get_mut(&(view_id, image_id.clone())) { - if overlay_item.id == *overlay_id { - self.overlays.remove(&(view_id, image_id.clone())); - } - } - } - pub(crate) fn get_image_overlay( &self, view_id: ViewId, @@ -160,7 +142,7 @@ impl Overlays { self.overlays.get_mut(&(view_id, image_id.clone())) } - pub(crate) fn clear_overlays(&mut self, view_id: ViewId, image_id: &ViewableObjectId) { + pub(crate) fn clear_overlay(&mut self, view_id: ViewId, image_id: &ViewableObjectId) { self.overlays.remove(&(view_id, image_id.clone())); } } diff --git a/src/webview-ui/src/application_state/vscode_data_fetcher.rs b/src/webview-ui/src/application_state/vscode_data_fetcher.rs index aa1719d6..b9820115 100644 --- a/src/webview-ui/src/application_state/vscode_data_fetcher.rs +++ b/src/webview-ui/src/application_state/vscode_data_fetcher.rs @@ -7,7 +7,7 @@ use yewdux::{Dispatch, Listener}; use anyhow::Result; use crate::{ - application_state::images::ImageAvailability, bindings::lodash, common::constants, + application_state::images::{DrawingContext, ImageAvailability}, bindings::lodash, common::constants, configurations::AutoUpdateImages, vscode::vscode_requests::VSCodeRequests, }; @@ -77,9 +77,8 @@ impl ImagesFetcher { let current_index = state .drawing_options .borrow() - .get_or_default(&image_id) - .batch_item - // batch item is not set, so we default to 0 (first time we see the image) + .get(&image_id, &DrawingContext::BaseImage) + .and_then(|d| d.batch_item) .unwrap_or(0); if let ImageAvailability::NotAvailable = current { @@ -111,8 +110,9 @@ impl ImagesFetcher { let current_drawing_options = state .drawing_options .borrow() - .get_or_default(&image_id) - .clone(); + .get(&image_id, &DrawingContext::BaseImage) + .cloned() + .unwrap_or_default(); if let Some(item) = current_drawing_options.batch_item { let has_item = image.borrow().textures.contains_key(&item); diff --git a/src/webview-ui/src/coloring.rs b/src/webview-ui/src/coloring.rs index 7ad774d2..35b64984 100644 --- a/src/webview-ui/src/coloring.rs +++ b/src/webview-ui/src/coloring.rs @@ -33,6 +33,7 @@ pub(crate) struct DrawingOptions { pub ignore_alpha: bool, pub batch_item: Option, pub clip: Clip, + pub zeros_as_transparent: bool, } impl Default for DrawingOptions { @@ -44,6 +45,7 @@ impl Default for DrawingOptions { ignore_alpha: false, batch_item: None, clip: Clip::default(), + zeros_as_transparent: false, } } } diff --git a/src/webview-ui/src/components/colorbar.rs b/src/webview-ui/src/components/colorbar.rs index 18dffc1a..434aa500 100644 --- a/src/webview-ui/src/components/colorbar.rs +++ b/src/webview-ui/src/components/colorbar.rs @@ -3,7 +3,10 @@ use yew::prelude::*; use yewdux::{functional::use_selector, Dispatch}; use crate::{ - application_state::app_state::{AppState, ElementsStoreKey, StoreAction, UpdateDrawingOptions}, + application_state::{ + app_state::{AppState, ElementsStoreKey, StoreAction, UpdateDrawingOptions}, + images::DrawingContext, + }, common::ViewableObjectId, hooks::{use_drag, UseDragOptions}, }; @@ -279,10 +282,12 @@ pub fn Colorbar(props: &ColorbarProps) -> Html { }; dispatch.apply(StoreAction::UpdateDrawingOptions( image_id.clone(), + DrawingContext::BaseImage, UpdateDrawingOptions::ClipMin(Some(clip_min)), )); dispatch.apply(StoreAction::UpdateDrawingOptions( image_id.clone(), + DrawingContext::BaseImage, UpdateDrawingOptions::ClipMax(Some(clip_max)), )); } diff --git a/src/webview-ui/src/components/display_options.rs b/src/webview-ui/src/components/display_options.rs index 09ca372e..199f62f3 100644 --- a/src/webview-ui/src/components/display_options.rs +++ b/src/webview-ui/src/components/display_options.rs @@ -3,17 +3,20 @@ use yew::prelude::*; use yewdux::{prelude::use_selector, Dispatch}; use crate::{ - application_state::app_state::{AppState, StoreAction, UpdateDrawingOptions}, + application_state::{ + app_state::{AppState, StoreAction, UpdateDrawingOptions}, + images::DrawingContext, + }, coloring::Coloring, common::ImageInfo, }; use super::icon_button::IconButton; - #[derive(PartialEq, Properties)] pub(crate) struct DisplayOptionProps { pub entry: ImageInfo, + pub drawing_context: DrawingContext, } mod features { @@ -94,11 +97,20 @@ mod features { #[function_component] pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { - let DisplayOptionProps { entry } = props; + let DisplayOptionProps { + entry, + drawing_context, + } = props; + let drawing_context = *drawing_context; let image_id = entry.image_id.clone(); let drawing_options = use_selector(move |state: &AppState| { - state.drawing_options.borrow().get_or_default(&image_id) + state + .drawing_options + .borrow() + .get(&image_id, &drawing_context) + .cloned() + .unwrap_or_default() }); let features = features::list_features(entry); @@ -119,6 +131,17 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { let default_style = use_style!(r#" "#); let image_id = entry.image_id.clone(); + let make_drawing_options_update = |update: UpdateDrawingOptions| { + let image_id = image_id.clone(); + let dispatch = Dispatch::::global(); + Callback::from(move |_| { + dispatch.apply(StoreAction::UpdateDrawingOptions( + image_id.clone(), + drawing_context, + update.clone(), + )); + }) + }; let reset_button = html! { Html { aria_label={"Reset"} title={"Reset"} icon={"codicon codicon-discard"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Reset)); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Reset)} /> }; let high_contrast_button = html! { @@ -142,12 +161,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"High Contrast"} title={"High Contrast"} icon={"svifpd-icons svifpd-icons-contrast"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - let drawing_options = drawing_options.clone(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::HighContrast(!drawing_options.high_contrast))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::HighContrast(!drawing_options.high_contrast))} /> }; let grayscale_button = html! { @@ -161,11 +175,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Grayscale"} title={"Grayscale"} icon={"svifpd-icons svifpd-icons-grayscale"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::Grayscale))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::Grayscale))} /> }; let swap_rgb_bgr_button = html! { @@ -177,11 +187,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Swap RGB/BGR"} title={"Swap RGB/BGR"} icon={"svifpd-icons svifpd-icons-BGR"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::SwapRgbBgr))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::SwapRgbBgr))} /> }; let r_button = html! { @@ -193,11 +199,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Red Channel"} title={"Red Channel"} icon={"svifpd-icons svifpd-icons-R"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::R))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::R))} /> }; let g_button = html! { @@ -209,11 +211,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Green Channel"} title={"Green Channel"} icon={"svifpd-icons svifpd-icons-G"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::G))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::G))} /> }; let b_button = html! { @@ -225,11 +223,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Blue Channel"} title={"Blue Channel"} icon={"svifpd-icons svifpd-icons-B"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::B))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::B))} /> }; let invert_button = html! { @@ -241,12 +235,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Invert Colors"} title={"Invert Colors"} icon={"svifpd-icons svifpd-icons-invert"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - let drawing_options = drawing_options.clone(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Invert(!drawing_options.invert))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Invert(!drawing_options.invert))} /> }; let ignore_alpha_button = html! { @@ -258,12 +247,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Ignore Alpha"} title={"Ignore Alpha"} icon={"svifpd-icons svifpd-icons-toggle-transparency"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - let drawing_options = drawing_options.clone(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::IgnoreAlpha(!drawing_options.ignore_alpha))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::IgnoreAlpha(!drawing_options.ignore_alpha))} /> }; let heatmap_button = html! { @@ -275,11 +259,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Heatmap"} title={"Heatmap"} icon={"svifpd-icons svifpd-icons-heatmap"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::Heatmap))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::Heatmap))} /> }; let segmentation_button = html! { @@ -291,11 +271,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { aria_label={"Segmentation"} title={"Segmentation"} icon={"svifpd-icons svifpd-icons-segmentation"} - onclick={{ - let image_id = image_id.clone(); - let dispatch = Dispatch::::global(); - move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions(image_id.clone(), UpdateDrawingOptions::Coloring(Coloring::Segmentation))); } - }} + onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::Segmentation))} /> }; // let tensor_button = html! { @@ -383,4 +359,3 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html {
} } - diff --git a/src/webview-ui/src/components/image_list_item.rs b/src/webview-ui/src/components/image_list_item.rs index 3e81a484..0d35e2aa 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -4,7 +4,7 @@ use yew::prelude::*; use yewdux::Dispatch; use crate::{ - application_state::app_state::{AppState, OverlayAction, UiAction}, + application_state::{app_state::{AppState, OverlayAction, UiAction}, images::DrawingContext}, common::{Image, MinimalImageInfo, ValueVariableKind, ViewId}, components::{ context_menu::{use_context_menu, ContextMenuData, ContextMenuItem}, @@ -209,7 +209,7 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html {
if let Image::Full(entry) = entry { - if *selected {} else {<>} + if *selected {} else {<>} } else {<>}
} diff --git a/src/webview-ui/src/components/image_selection_list.rs b/src/webview-ui/src/components/image_selection_list.rs index 8d07d0f5..009ec58d 100644 --- a/src/webview-ui/src/components/image_selection_list.rs +++ b/src/webview-ui/src/components/image_selection_list.rs @@ -5,7 +5,7 @@ use yew::prelude::*; use yewdux::prelude::*; use crate::{ - application_state::app_state::{AppState, StoreAction}, + application_state::{app_state::{AppState, StoreAction}, images::DrawingContext}, common::{CurrentlyViewing, Image, ViewId}, components::image_list_item::ImageListItem, }; @@ -69,7 +69,7 @@ fn ImageItemWrapper(props: &ImageItemWrapperProps) -> Html { state .drawing_options .borrow() - .get(&image_id) + .get(&image_id, &DrawingContext::BaseImage) .and_then(|d| d.batch_item) } }); diff --git a/src/webview-ui/src/components/main.rs b/src/webview-ui/src/components/main.rs index a2044dad..52af1b5e 100644 --- a/src/webview-ui/src/components/main.rs +++ b/src/webview-ui/src/components/main.rs @@ -6,7 +6,7 @@ use yew::prelude::*; use yewdux::{prelude::Dispatch, use_selector}; use crate::{ - application_state::app_state::AppState, + application_state::{app_state::AppState, images::DrawingContext}, common::{pixel_value::PixelValue, AppMode, ViewId}, components::{ main_toolbar::MainToolbar, sidebar::Sidebar, status_bar::StatusBar, @@ -72,7 +72,7 @@ fn StatusBarWrapper(props: &StatusBarWrapperProps) -> Html { .get() .drawing_options .borrow() - .get(&image.info.image_id) + .get(&image.info.image_id, &DrawingContext::BaseImage) .and_then(|d| d.batch_item) .unwrap_or(0); diff --git a/src/webview-ui/src/components/main_toolbar.rs b/src/webview-ui/src/components/main_toolbar.rs index 140f4ed6..43673b9f 100644 --- a/src/webview-ui/src/components/main_toolbar.rs +++ b/src/webview-ui/src/components/main_toolbar.rs @@ -6,11 +6,12 @@ use stylist::{ use wasm_bindgen::JsCast; use yew::prelude::*; -use yewdux::{dispatch, prelude::use_selector, use_selector_with_deps, Dispatch}; +use yewdux::{prelude::use_selector, use_selector_with_deps, Dispatch}; use crate::{ application_state::{ app_state::{AppState, OverlayAction, StoreAction, UpdateGlobalDrawingOptions}, + images::DrawingContext, views::OverlayItem, }, coloring::Coloring, @@ -219,16 +220,13 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { let alpha_throttle = { let alpha_state = alpha_state.clone(); let cv_image_id = cv_image_id.clone(); - yew_hooks::use_throttle( - move || { - dispatch.apply(OverlayAction::SetAlpha { - view_id: overlay.view_id, - image_id: cv_image_id.clone(), - alpha: *alpha_state, - }); - }, - 10, - ) + move || { + dispatch.apply(OverlayAction::SetAlpha { + view_id: overlay.view_id, + image_id: cv_image_id.clone(), + alpha: *alpha_state, + }); + } }; let alpha_slider = html! { Html { .value(); if let Ok(value) = value.parse::() { alpha_state.set(value); - alpha_throttle.run(); + alpha_throttle(); } } }) @@ -259,7 +257,7 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { }; let display_options = html! { - + }; let style = use_style!( @@ -289,6 +287,23 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { background-color: var(--vscode-sideBar-background); border: 1px solid var(--vscode-panel-border); padding: 5px; + min-width: max-content; + } + + &[data-hidden="true"] .controls-container { + display: none; + } + + .slider-container { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 0.3em 0; + } + + .slider-container label { + font-size: 0.75rem; + line-height: 0.6em; } .slider { @@ -299,7 +314,10 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { ); html! { -
+
{show_hide_button} @@ -312,10 +330,8 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html {
{ display_options }
-
- - {"Alpha"} - +
+ { alpha_slider }
@@ -340,7 +356,13 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { |state: &AppState, cv| { cv.as_ref() .as_ref() - .map(|cv| state.drawing_options.borrow().get_or_default(cv.id())) + .and_then(|cv| { + state + .drawing_options + .borrow() + .get(cv.id(), &DrawingContext::BaseImage) + .cloned() + }) .unwrap_or_default() }, cv.clone(), @@ -348,7 +370,7 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { let app_mode = use_selector(|state: &AppState| state.app_mode); - let cv_image_info = use_selector_with_deps( + let cv_image_info_in_single_mode = use_selector_with_deps( |state: &AppState, (cv, app_mode)| { if **app_mode == AppMode::SingleImage { cv.as_ref() @@ -467,10 +489,11 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { html! {
- if let Some(ref cv_image_info) = cv_image_info.as_ref() { + if let Some(ref cv_image_info) = cv_image_info_in_single_mode.as_ref() { if let Image::Full(image) = cv_image_info { } } diff --git a/src/webview-ui/src/components/view_container.rs b/src/webview-ui/src/components/view_container.rs index e8f9f561..9f00fc4e 100644 --- a/src/webview-ui/src/components/view_container.rs +++ b/src/webview-ui/src/components/view_container.rs @@ -9,7 +9,7 @@ use yewdux::{functional::use_selector, Dispatch}; use crate::{ application_state::{ app_state::{AppState, StoreAction, UpdateDrawingOptions}, - images::ImageAvailability, + images::{DrawingContext, ImageAvailability}, vscode_data_fetcher::ImagesFetcher, }, coloring::{self, Coloring, DrawingOptions}, @@ -51,8 +51,10 @@ pub fn ClippingInput(props: &ClippingInputProps) -> Html { state .drawing_options .borrow() - .get_or_default(&image_id) - .clip + .get(&image_id, &DrawingContext::BaseImage) + .map(|d| &d.clip) + .cloned() + .unwrap_or_default() }) }; let style = use_style!( @@ -98,6 +100,7 @@ pub fn ClippingInput(props: &ClippingInputProps) -> Html { let value: Option = value_str.parse().ok(); dispatch.apply(StoreAction::UpdateDrawingOptions( image_id.clone(), + DrawingContext::BaseImage, match clip { ClippingInputType::Min => UpdateDrawingOptions::ClipMin(value), ClippingInputType::Max => UpdateDrawingOptions::ClipMax(value), @@ -112,6 +115,7 @@ pub fn ClippingInput(props: &ClippingInputProps) -> Html { Callback::from(move |_| { dispatch.apply(StoreAction::UpdateDrawingOptions( image_id.clone(), + DrawingContext::BaseImage, match clip { ClippingInputType::Min => UpdateDrawingOptions::ClipMin(None), ClippingInputType::Max => UpdateDrawingOptions::ClipMax(None), @@ -294,25 +298,25 @@ pub fn ColorbarContainer(props: &ColorbarContainerProps) -> Html { let current_image = { let view_id = *view_id; use_selector( - move |state: &AppState| -> Option<(ViewableObjectId, ImageAvailability, Option)> { + move |state: &AppState| -> Option<(ViewableObjectId, ImageAvailability, DrawingOptions)> { let binding = state.image_views.borrow().get_currently_viewing(view_id)?; let image_id = binding.id(); let availability = state.image_cache.borrow().get(image_id); - let drawing_options = state.drawing_options.borrow().get(image_id); + let drawing_options = state.drawing_options.borrow().get(image_id, &DrawingContext::BaseImage).cloned().unwrap_or_default(); Some((image_id.clone(), availability, drawing_options)) }, ) }; if let Some((_, availability, drawing_options)) = current_image.as_ref() { - if drawing_options.as_ref().map(|o| o.coloring) == Some(Coloring::Heatmap) { + if drawing_options.coloring == Coloring::Heatmap { if let ImageAvailability::Available(texture) = availability { let image_info = &texture.borrow().computed_info; let image_id = texture.borrow().info.image_id.clone(); let min = image_info.min.as_rgba_f32()[0]; let max = image_info.max.as_rgba_f32()[0]; - let clip_min = drawing_options.as_ref().and_then(|o| o.clip.min); - let clip_max = drawing_options.as_ref().and_then(|o| o.clip.max); + let clip_min = drawing_options.clip.min; + let clip_max = drawing_options.clip.max; return html! { @@ -345,13 +349,19 @@ pub(crate) fn ViewContainer(props: &ViewContainerProps) -> Html { move |state: &AppState| -> Option<( ViewableObjectId, ImageAvailability, - Option, + DrawingOptions, bool, )> { let binding = state.image_views.borrow().get_currently_viewing(view_id)?; let image_id = binding.id(); let availability = state.image_cache.borrow().get(image_id); - let drawing_options = state.drawing_options.borrow().get(image_id); + let drawing_options = state + .drawing_options + .borrow() + .get(image_id, &DrawingContext::BaseImage) + .cloned() + .unwrap_or_default(); + let is_batch_item = matches!(binding, CurrentlyViewing::BatchItem(_)); Some(( image_id.clone(), @@ -370,9 +380,7 @@ pub(crate) fn ViewContainer(props: &ViewContainerProps) -> Html { .as_ref() .as_ref() .map(|(_, availability, drawing_options, is_batch_item)| { - let batch_item = is_batch_item - .then(|| drawing_options.as_ref()?.batch_item) - .flatten(); + let batch_item = is_batch_item.then(|| drawing_options.batch_item).flatten(); match (batch_item, availability) { (Some(item), ImageAvailability::Available(image)) => { @@ -402,8 +410,7 @@ pub(crate) fn ViewContainer(props: &ViewContainerProps) -> Html { let info_items = if let Some((image_id, availability, drawing_options, _)) = current_image.as_ref() { - let drawing_options = drawing_options.clone().unwrap_or_default(); - make_info_items(image_id, availability, &drawing_options) + make_info_items(image_id, availability, drawing_options) } else { None } diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index 7190dbce..1beb5963 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -10,6 +10,7 @@ use glam::{Mat3, UVec2, Vec2, Vec4}; use web_sys::{WebGl2RenderingContext as GL, WebGl2RenderingContext}; use crate::application_state::app_state::GlobalDrawingOptions; +use crate::application_state::images::DrawingContext; use crate::application_state::images::ImageAvailability; use crate::application_state::views::OverlayItem; use crate::coloring; @@ -293,7 +294,7 @@ impl ImageRenderer { // for batch, we need to check if the batch item is available let batch_index = if matches!(cv, CurrentlyViewing::BatchItem(_)) { let batch_index = rendering_context - .drawing_options(image_id) + .drawing_options(image_id, &DrawingContext::BaseImage) .0 .batch_item .filter(|i| texture.borrow().textures.contains_key(i)); @@ -391,6 +392,7 @@ impl ImageRenderer { } } + #[allow(clippy::too_many_arguments)] fn prepare_texture_uniforms<'a>( rendering_context: &dyn RenderingContext, rendering_data: &'a RenderingData, @@ -398,6 +400,7 @@ impl ImageRenderer { colormap_texture: Option<&'a web_sys::WebGlTexture>, batch_item: Option, image_view_data: &ImageViewData, + drawing_context: &DrawingContext, uniform_values: &mut HashMap<&'static str, UniformValue<'a>>, ) { let texture_info = &texture.info; @@ -422,13 +425,8 @@ impl ImageRenderer { let image_size = texture.image_size(); let image_size_vec = Vec2::new(image_size.width, image_size.height); - let (drawing_options, global_drawing_options) = rendering_context.drawing_options( - image_view_data - .currently_viewing - .as_ref() - .map(CurrentlyViewing::id) - .unwrap(), - ); + let image_id = &texture.info.image_id; + let (drawing_options, _) = rendering_context.drawing_options(image_id, drawing_context); let coloring_factors = calculate_color_matrix(texture_info, &texture.computed_info, &drawing_options); @@ -598,13 +596,14 @@ impl ImageRenderer { let config = rendering_context.rendering_configuration(); - let (drawing_options, global_drawing_options) = rendering_context.drawing_options( - image_view_data - .currently_viewing - .as_ref() - .map(CurrentlyViewing::id) - .unwrap(), - ); + let (drawing_options, global_drawing_options) = + rendering_context.drawing_options(&overlay_item.id, &DrawingContext::Overlay); + + // log::debug!( + // "Rendering overlay {:?} with drawing options: {:?}", + // overlay_item, + // drawing_options + // ); let colormap_texture = if Coloring::Heatmap == drawing_options.coloring { let color_map_texture = rendering_context @@ -631,12 +630,17 @@ impl ImageRenderer { colormap_texture.as_ref(), batch_item, image_view_data, + &DrawingContext::Overlay, &mut uniform_values, ); // Overlay specific uniforms uniform_values.insert("u_is_overlay", UniformValue::Bool(&true)); uniform_values.insert("u_overlay_alpha", UniformValue::Float(&overlay_item.alpha)); + uniform_values.insert( + "u_zeros_as_transparent", + UniformValue::Bool(&drawing_options.zeros_as_transparent), + ); gl.use_program(Some(&program.program)); set_uniforms(program, &uniform_values); @@ -687,13 +691,15 @@ impl ImageRenderer { let program = ImageRenderer::program_for_texture(&texture, &rendering_data.programs); let config = rendering_context.rendering_configuration(); - let (drawing_options, global_drawing_options) = rendering_context.drawing_options( - image_view_data - .currently_viewing - .as_ref() - .map(CurrentlyViewing::id) - .unwrap(), - ); + let cv_id = image_view_data + .currently_viewing + .as_ref() + .map(CurrentlyViewing::id) + .unwrap_or_else(|| { + panic!("No currently viewing for image view data"); + }); + let (drawing_options, global_drawing_options) = + rendering_context.drawing_options(cv_id, &DrawingContext::BaseImage); let colormap_texture = if Coloring::Heatmap == drawing_options.coloring { let color_map_texture = rendering_context @@ -722,12 +728,17 @@ impl ImageRenderer { colormap_texture.as_ref(), batch_item, image_view_data, + &DrawingContext::BaseImage, &mut uniform_values, ); // Set the overlay specific uniforms uniform_values.insert("u_is_overlay", UniformValue::Bool(&false)); uniform_values.insert("u_overlay_alpha", UniformValue::Float(&0.0)); + uniform_values.insert( + "u_zeros_as_transparent", + UniformValue::Bool(&drawing_options.zeros_as_transparent), + ); gl.use_program(Some(&program.program)); set_uniforms(program, &uniform_values); diff --git a/src/webview-ui/src/rendering/rendering_context.rs b/src/webview-ui/src/rendering/rendering_context.rs index fef1b38a..6e1ceab6 100644 --- a/src/webview-ui/src/rendering/rendering_context.rs +++ b/src/webview-ui/src/rendering/rendering_context.rs @@ -1,11 +1,15 @@ use anyhow::Result; -use yewdux::mrc::Mrc; use std::rc::Rc; +use yewdux::mrc::Mrc; use web_sys::{HtmlElement, WebGl2RenderingContext}; use crate::{ - application_state::{app_state::GlobalDrawingOptions, images::ImageAvailability, views::OverlayItem}, + application_state::{ + app_state::GlobalDrawingOptions, + images::{DrawingContext, ImageAvailability}, + views::OverlayItem, + }, coloring::DrawingOptions, colormap, common::{ @@ -38,6 +42,7 @@ pub(crate) trait RenderingContext { fn drawing_options( &self, image_id: &ViewableObjectId, + drawing_context: &DrawingContext, ) -> (DrawingOptions, GlobalDrawingOptions); fn get_color_map(&self, name: &str) -> Result>; fn get_color_map_texture( From f4694005e524a7496c13e975b192386d1ced6e73 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:04:48 +0200 Subject: [PATCH 07/18] edges --- icons/dist/svifpd-icons.css | 3 +- icons/dist/svifpd-icons.html | 8 ++ icons/dist/svifpd-icons.svg | 2 +- icons/dist/svifpd-icons.woff2 | Bin 3900 -> 4108 bytes icons/src/icons/edges.svg | 1 + icons/src/template/mapping.json | 3 +- src/webview-ui/shaders/src/shader_parts.rs | 76 +++++++++++++++++- .../src/application_state/app_state.rs | 4 +- src/webview-ui/src/coloring.rs | 6 +- .../src/components/display_options.rs | 16 +++- .../src/rendering/image_renderer.rs | 18 +++-- 11 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 icons/src/icons/edges.svg diff --git a/icons/dist/svifpd-icons.css b/icons/dist/svifpd-icons.css index 9217a63d..9b075233 100644 --- a/icons/dist/svifpd-icons.css +++ b/icons/dist/svifpd-icons.css @@ -6,7 +6,7 @@ @font-face { font-family: "svifpd-icons"; font-display: block; - src: url("./svifpd-icons.woff2?797726f429734e5d9e1970c77951bcfd") format("woff2"); + src: url("./svifpd-icons.woff2?ddf2b6472b7dddb3c0d7e731d61b8fff") format("woff2"); } .svifpd-icons[class*='svifpd-icons-'] { @@ -71,3 +71,4 @@ .svifpd-icons-tensor:before { content: "\ea6c" } .svifpd-icons-image:before { content: "\ea6d" } .svifpd-icons-inspect-image:before { content: "\ea6e" } +.svifpd-icons-edges:before { content: "\ea6f" } diff --git a/icons/dist/svifpd-icons.html b/icons/dist/svifpd-icons.html index 7481892c..35a4bcc6 100644 --- a/icons/dist/svifpd-icons.html +++ b/icons/dist/svifpd-icons.html @@ -177,6 +177,14 @@

svifpd-icons

contrast
+
+ + + +
+ edges + +
diff --git a/icons/dist/svifpd-icons.svg b/icons/dist/svifpd-icons.svg index f2bd453d..e789c110 100644 --- a/icons/dist/svifpd-icons.svg +++ b/icons/dist/svifpd-icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/dist/svifpd-icons.woff2 b/icons/dist/svifpd-icons.woff2 index 4a02d06e53c4eedaaf9ae5ad2c8a8828006724d4..99bc909c751ca811af09b48b8514fbc990926a59 100644 GIT binary patch literal 4108 zcmV+n5cBVMPew8T0RR9101ylS3jhEB06VMz01vzX0RR9100000000000000000000 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 Kxmp;>>mdNlEQa?0 diff --git a/icons/src/icons/edges.svg b/icons/src/icons/edges.svg new file mode 100644 index 00000000..58989c4d --- /dev/null +++ b/icons/src/icons/edges.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/src/template/mapping.json b/icons/src/template/mapping.json index 9ad4941f..db283d7b 100644 --- a/icons/src/template/mapping.json +++ b/icons/src/template/mapping.json @@ -12,5 +12,6 @@ "legend": 60011, "tensor": 60012, "image": 60013, - "inspect-image": 60014 + "inspect-image": 60014, + "edges": 60015 } \ No newline at end of file diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs index a2fc7d66..237b57e6 100644 --- a/src/webview-ui/shaders/src/shader_parts.rs +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -32,6 +32,7 @@ uniform float u_max_clip_value; uniform bool u_is_overlay; uniform float u_overlay_alpha; uniform bool u_zeros_as_transparent; +uniform bool u_edges_only; uniform bool u_use_colormap; uniform sampler2D u_colormap; @@ -43,6 +44,9 @@ const float CHECKER_SIZE = 10.0; const float WHITE_CHECKER = 0.9; const float BLACK_CHECKER = 0.6; +// Thickness of the edge as a fraction of the pixel size +const float EDGE_THICKNESS = 0.2; + {ADDITIONAL_CONSTANTS} {ADDITIONAL_UNIFORMS} @@ -56,6 +60,70 @@ bool is_nan(float val) {{ return (val < 0. || 0. < val || val == 0.) ? false : true; }} +bool is_edge(vec2 uv) {{ + // Calculate the size of one pixel in texture coordinates + vec2 texel_size = 1.0 / u_buffer_dimension; + + // Sample the current pixel and its neighbors + // uint current = texture(u_texture, uv).r; + float current; + {{ + vec2 pix = uv; + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + // uint top = texture(u_texture, uv - vec2(0.0, texel_size.y)).r; + float top; + {{ + vec2 pix = uv - vec2(0.0, texel_size.y); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + // uint bottom = texture(u_texture, uv + vec2(0.0, texel_size.y)).r; + float bottom; + {{ + vec2 pix = uv + vec2(0.0, texel_size.y); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + // uint left = texture(u_texture, uv - vec2(texel_size.x, 0.0)).r; + float left; + {{ + vec2 pix = uv - vec2(texel_size.x, 0.0); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + // uint right = texture(u_texture, uv + vec2(texel_size.x, 0.0)).r; + float right; + {{ + vec2 pix = uv + vec2(texel_size.x, 0.0); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + + bool is_left_border = (current != left); + bool is_right_border = (current != right); + bool is_top_border = (current != top); + bool is_bottom_border = (current != bottom); + + // Calculate the position within the pixel + vec2 pixel_position = fract(uv * u_buffer_dimension); + + bool is_top_edge = pixel_position.y < EDGE_THICKNESS && is_top_border; + bool is_bottom_edge = + pixel_position.y > (1.0 - EDGE_THICKNESS) && is_bottom_border; + bool is_left_edge = pixel_position.x < EDGE_THICKNESS && is_left_border; + bool is_right_edge = + pixel_position.x > (1.0 - EDGE_THICKNESS) && is_right_border; + + return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge; +}} + {ADDITIONAL_FUNCTIONS} @@ -109,7 +177,13 @@ void main() {{ }} }} - if (u_zeros_as_transparent && sampled.r == 0. && sampled.g == 0. && sampled.b == 0.) {{ + if (u_edges_only) {{ + if (!is_edge(vout_uv)) {{ + color = vec4(0.0, 0.0, 0.0, 1.0); + }} + }} + + if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) {{ color.a = 0.0; }} diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index beee6a3c..94000539 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -298,8 +298,8 @@ impl Reducer for StoreAction { batch_item: current_drawing_options.batch_item, ..DrawingOptions::default() }, - UpdateDrawingOptions::Coloring(Coloring::Segmentation) => DrawingOptions { - coloring: Coloring::Segmentation, + UpdateDrawingOptions::Coloring(coloring @ (Coloring::Segmentation | Coloring::Edges)) => DrawingOptions { + coloring, zeros_as_transparent: if drawing_context == DrawingContext::BaseImage { current_drawing_options.zeros_as_transparent } else { diff --git a/src/webview-ui/src/coloring.rs b/src/webview-ui/src/coloring.rs index 35b64984..2e92da99 100644 --- a/src/webview-ui/src/coloring.rs +++ b/src/webview-ui/src/coloring.rs @@ -14,6 +14,7 @@ pub(crate) enum Coloring { B, SwapRgbBgr, Segmentation, + Edges, Heatmap, } @@ -210,8 +211,9 @@ pub(crate) fn calculate_color_matrix( let (reorder, reorder_add) = match drawing_options.coloring { | Coloring::Default // Heatmap and Segmentation coloring using the default coloring - | Coloring::Segmentation - | Coloring::Heatmap + | Coloring::Segmentation + | Coloring::Edges + | Coloring::Heatmap => { match datatype { | Datatype::Uint8 diff --git a/src/webview-ui/src/components/display_options.rs b/src/webview-ui/src/components/display_options.rs index 199f62f3..6d0970c2 100644 --- a/src/webview-ui/src/components/display_options.rs +++ b/src/webview-ui/src/components/display_options.rs @@ -55,6 +55,7 @@ mod features { let gray_features = Feature::HighContrast | Feature::Heatmap | alpha_features; let integer_gray_features = Feature::Segmentation | gray_features; // let batched_features = EnumSet::only(Feature::Batched); + let binary_features = EnumSet::only(Feature::Segmentation); let no_additional_features = EnumSet::empty(); for_all | match (channels, datatype) { @@ -65,7 +66,7 @@ mod features { (Channels::One, Datatype::Int8) => integer_gray_features, (Channels::One, Datatype::Int16) => integer_gray_features, (Channels::One, Datatype::Int32) => integer_gray_features, - (Channels::One, Datatype::Bool) => no_additional_features, + (Channels::One, Datatype::Bool) => binary_features, (Channels::Two, Datatype::Uint8) => gray_alpha_features, (Channels::Two, Datatype::Uint16) => gray_alpha_features, (Channels::Two, Datatype::Uint32) => gray_alpha_features, @@ -274,6 +275,18 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { onclick={make_drawing_options_update(UpdateDrawingOptions::Coloring(Coloring::Segmentation))} /> }; + let edges_button = html! { + + }; // let tensor_button = html! { // Html { } if features.contains(features::Feature::Segmentation) { buttons.push(segmentation_button); + buttons.push(edges_button); } // if features.contains(features::Feature::Transpose) { // buttons.push(transpose_button); diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index 1beb5963..ba5890fa 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -453,6 +453,11 @@ impl ImageRenderer { let batch_index = batch_item.unwrap_or(0); uniform_values.extend(ImageRenderer::get_texture_uniforms(texture, batch_index)); + uniform_values.insert( + "u_edges_only", + UniformValue::Bool_(drawing_options.coloring == Coloring::Edges), + ); + if let Some(colormap_texture) = colormap_texture { uniform_values.insert("u_colormap", UniformValue::Texture(colormap_texture)); uniform_values.insert("u_use_colormap", UniformValue::Bool(&true)); @@ -464,9 +469,6 @@ impl ImageRenderer { ); } - // let texture_size = Vec2::new(texture.image_size().width, texture.image_size().height); - // uniform_values.insert("u_texture_size", UniformValue::Vec2(&texture_size)); - if texture_info.channels == Channels::One { if let Some(clip_min) = drawing_options.clip.min { uniform_values.insert("u_clip_min", UniformValue::Bool(&true)); @@ -611,7 +613,10 @@ impl ImageRenderer { .expect("Could not get color map texture"); Some(color_map_texture.obj.clone()) - } else if Coloring::Segmentation == drawing_options.coloring { + } else if matches!( + drawing_options.coloring, + Coloring::Segmentation | Coloring::Edges + ) { let color_map_texture = rendering_context .get_color_map_texture(&global_drawing_options.segmentation_colormap_name) .expect("Could not get color map texture"); @@ -708,7 +713,10 @@ impl ImageRenderer { let tex = color_map_texture.obj.clone(); Some(tex) - } else if Coloring::Segmentation == drawing_options.coloring { + } else if matches!( + drawing_options.coloring, + Coloring::Segmentation | Coloring::Edges + ) { let color_map_texture = rendering_context .get_color_map_texture(&global_drawing_options.segmentation_colormap_name) .expect("Could not get color map texture"); From 4b8e1ab0ce2af408953024be8c95142aa7f3c4bc Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:04:48 +0200 Subject: [PATCH 08/18] Enhance edge detection with dynamic thickness based on pixel size --- src/webview-ui/shaders/src/shader_parts.rs | 15 ++++++++------- src/webview-ui/src/application_state/views.rs | 4 +--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs index 237b57e6..3d3d5b10 100644 --- a/src/webview-ui/shaders/src/shader_parts.rs +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -113,13 +113,14 @@ bool is_edge(vec2 uv) {{ // Calculate the position within the pixel vec2 pixel_position = fract(uv * u_buffer_dimension); - - bool is_top_edge = pixel_position.y < EDGE_THICKNESS && is_top_border; - bool is_bottom_edge = - pixel_position.y > (1.0 - EDGE_THICKNESS) && is_bottom_border; - bool is_left_edge = pixel_position.x < EDGE_THICKNESS && is_left_border; - bool is_right_edge = - pixel_position.x > (1.0 - EDGE_THICKNESS) && is_right_border; + // New: compute image-pixel size in screen-pixels and a dynamic threshold. + float inv_derivative = 1.0 / max(abs(dFdx(uv * u_buffer_dimension).x), abs(dFdy(uv * u_buffer_dimension).y)); + float dynamic_thickness = max(inv_derivative * EDGE_THICKNESS, 1.0); + + bool is_top_edge = (pixel_position.y * inv_derivative < dynamic_thickness) && is_top_border; + bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; + bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; + bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge; }} diff --git a/src/webview-ui/src/application_state/views.rs b/src/webview-ui/src/application_state/views.rs index 997971e6..ba921a33 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -93,7 +93,6 @@ pub(crate) struct OverlayItem { pub(crate) id: ViewableObjectId, pub(crate) hidden: bool, pub(crate) alpha: f32, - pub(crate) only_edges: bool, } impl OverlayItem { @@ -102,8 +101,7 @@ impl OverlayItem { view_id, id, hidden: false, - alpha: 0.4, - only_edges: false, + alpha: 0.8, } } } From 9cec6e71ca10201ae023402b165d54efef0c51a2 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:04:48 +0200 Subject: [PATCH 09/18] Enhance edge detection logic and update text color for edges in image renderer --- src/webview-ui/shaders/src/shader_parts.rs | 71 +++++++++++++------ .../src/rendering/image_renderer.rs | 4 ++ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/webview-ui/shaders/src/shader_parts.rs b/src/webview-ui/shaders/src/shader_parts.rs index 3d3d5b10..5955269f 100644 --- a/src/webview-ui/shaders/src/shader_parts.rs +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -65,7 +65,6 @@ bool is_edge(vec2 uv) {{ vec2 texel_size = 1.0 / u_buffer_dimension; // Sample the current pixel and its neighbors - // uint current = texture(u_texture, uv).r; float current; {{ vec2 pix = uv; @@ -73,7 +72,6 @@ bool is_edge(vec2 uv) {{ {SAMPLE_CODE} current = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE }} - // uint top = texture(u_texture, uv - vec2(0.0, texel_size.y)).r; float top; {{ vec2 pix = uv - vec2(0.0, texel_size.y); @@ -81,7 +79,6 @@ bool is_edge(vec2 uv) {{ {SAMPLE_CODE} top = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE }} - // uint bottom = texture(u_texture, uv + vec2(0.0, texel_size.y)).r; float bottom; {{ vec2 pix = uv + vec2(0.0, texel_size.y); @@ -89,7 +86,6 @@ bool is_edge(vec2 uv) {{ {SAMPLE_CODE} bottom = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE }} - // uint left = texture(u_texture, uv - vec2(texel_size.x, 0.0)).r; float left; {{ vec2 pix = uv - vec2(texel_size.x, 0.0); @@ -97,7 +93,6 @@ bool is_edge(vec2 uv) {{ {SAMPLE_CODE} left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE }} - // uint right = texture(u_texture, uv + vec2(texel_size.x, 0.0)).r; float right; {{ vec2 pix = uv + vec2(texel_size.x, 0.0); @@ -105,11 +100,43 @@ bool is_edge(vec2 uv) {{ {SAMPLE_CODE} right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE }} + float top_left; + {{ + vec2 pix = uv - texel_size; + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + top_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + float top_right; + {{ + vec2 pix = uv + vec2(texel_size.x, -texel_size.y); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + top_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + float bottom_left; + {{ + vec2 pix = uv + vec2(-texel_size.x, texel_size.y); + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + bottom_left = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} + float bottom_right; + {{ + vec2 pix = uv + texel_size; + vec4 sampled = vec4(0., 0., 0., 1.); + {SAMPLE_CODE} + bottom_right = sampled.r; // Assuming sampled is defined in the SAMPLE_CODE + }} bool is_left_border = (current != left); bool is_right_border = (current != right); bool is_top_border = (current != top); bool is_bottom_border = (current != bottom); + bool is_top_left_border = (current != top_left); + bool is_top_right_border = (current != top_right); + bool is_bottom_left_border = (current != bottom_left); + bool is_bottom_right_border = (current != bottom_right); // Calculate the position within the pixel vec2 pixel_position = fract(uv * u_buffer_dimension); @@ -121,8 +148,23 @@ bool is_edge(vec2 uv) {{ bool is_bottom_edge = ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness) && is_bottom_border; bool is_left_edge = (pixel_position.x * inv_derivative < dynamic_thickness) && is_left_border; bool is_right_edge = ((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && is_right_border; - - return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge; + bool is_top_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && + (pixel_position.y * inv_derivative < dynamic_thickness)) && + is_top_left_border; + bool is_top_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && + (pixel_position.y * inv_derivative < dynamic_thickness)) && + is_top_right_border; + bool is_bottom_left_edge = ((pixel_position.x * inv_derivative < dynamic_thickness) && + ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && + is_bottom_left_border; + bool is_bottom_right_edge = (((1.0 - pixel_position.x) * inv_derivative < dynamic_thickness) && + ((1.0 - pixel_position.y) * inv_derivative < dynamic_thickness)) && + is_bottom_right_border; + + // Return true if any edge condition is met + return is_top_edge || is_bottom_edge || is_left_edge || is_right_edge || + is_top_left_edge || is_top_right_edge || is_bottom_left_edge || + is_bottom_right_edge; }} {ADDITIONAL_FUNCTIONS} @@ -194,20 +236,7 @@ void main() {{ }} vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) {{ - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) {{ - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) {{ - discard; - }} - }} - + if (u_enable_borders && !u_is_overlay) {{ float alpha = max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); float x_ = fract(buffer_position.x); diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index ba5890fa..bc5e6cf1 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -542,6 +542,10 @@ impl ImageRenderer { // The actual pixel color might be different from the pixel value, depending on drawing options let text_color = match drawing_options.coloring { + Coloring::Edges => { + // for edges, the background color is always black + text_color(Vec4::new(0.0, 0.0, 0.0, 1.0), drawing_options) + }, Coloring::Heatmap | Coloring::Segmentation => { let name = match drawing_options.coloring { Coloring::Heatmap => &global_drawing_options.heatmap_colormap_name, From 108f5b3eb96a525511bb7d1bbfd5ae782e304256 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:04:48 +0200 Subject: [PATCH 10/18] Refactor overlay alpha handling and improve drawing options management --- .../src/application_state/app_state.rs | 58 +++++++++++-------- src/webview-ui/src/application_state/views.rs | 2 - src/webview-ui/src/coloring.rs | 2 + .../src/components/display_options.rs | 21 ++++--- src/webview-ui/src/components/main_toolbar.rs | 39 ++++++++----- .../src/rendering/image_renderer.rs | 9 ++- 6 files changed, 79 insertions(+), 52 deletions(-) diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index 94000539..696b1865 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -298,7 +298,9 @@ impl Reducer for StoreAction { batch_item: current_drawing_options.batch_item, ..DrawingOptions::default() }, - UpdateDrawingOptions::Coloring(coloring @ (Coloring::Segmentation | Coloring::Edges)) => DrawingOptions { + UpdateDrawingOptions::Coloring( + coloring @ (Coloring::Segmentation | Coloring::Edges), + ) => DrawingOptions { coloring, zeros_as_transparent: if drawing_context == DrawingContext::BaseImage { current_drawing_options.zeros_as_transparent @@ -544,7 +546,6 @@ pub(crate) enum OverlayAction { image_id: ViewableObjectId, }, SetAlpha { - view_id: ViewId, image_id: ViewableObjectId, alpha: f32, }, @@ -560,10 +561,25 @@ impl Reducer for OverlayAction { image_id, overlay_id, } => { - state - .overlays + state.overlays.borrow_mut().add_overlay_to_image( + view_id, + image_id, + overlay_id.clone(), + ); + + // init with 0.8 global alpha + if state + .drawing_options .borrow_mut() - .add_overlay_to_image(view_id, image_id, overlay_id); + .get(&overlay_id, &DrawingContext::Overlay) + .is_none() + { + state + .drawing_options + .borrow_mut() + .get_mut_ref(overlay_id, DrawingContext::Overlay) + .global_alpha = 0.8; + } } OverlayAction::Hide { view_id, @@ -586,26 +602,20 @@ impl Reducer for OverlayAction { overlay_item.hidden = false; } } - OverlayAction::SetAlpha { - view_id, - image_id, - alpha, - } => { - if let Some(overlay_item) = state - .overlays - .borrow_mut() - .get_image_overlay_mut(view_id, &image_id) - { - let mut alpha = alpha; - // Clamp alpha to [0.0, 1.0] range, with a threshold to avoid flickering - if alpha < 0.02 { - alpha = 0.0; - } - if alpha > 0.98 { - alpha = 1.0; - } - overlay_item.alpha = alpha; + OverlayAction::SetAlpha { image_id, alpha } => { + let mut alpha = alpha; + // Clamp alpha to [0.0, 1.0] range, with a threshold to avoid flickering + if alpha < 0.02 { + alpha = 0.0; + } + if alpha > 0.98 { + alpha = 1.0; } + state + .drawing_options + .borrow_mut() + .get_mut_ref(image_id, DrawingContext::Overlay) + .global_alpha = alpha; } } diff --git a/src/webview-ui/src/application_state/views.rs b/src/webview-ui/src/application_state/views.rs index ba921a33..badb7612 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -92,7 +92,6 @@ pub(crate) struct OverlayItem { pub(crate) view_id: ViewId, pub(crate) id: ViewableObjectId, pub(crate) hidden: bool, - pub(crate) alpha: f32, } impl OverlayItem { @@ -101,7 +100,6 @@ impl OverlayItem { view_id, id, hidden: false, - alpha: 0.8, } } } diff --git a/src/webview-ui/src/coloring.rs b/src/webview-ui/src/coloring.rs index 2e92da99..507c8b50 100644 --- a/src/webview-ui/src/coloring.rs +++ b/src/webview-ui/src/coloring.rs @@ -35,6 +35,7 @@ pub(crate) struct DrawingOptions { pub batch_item: Option, pub clip: Clip, pub zeros_as_transparent: bool, + pub global_alpha: f32, } impl Default for DrawingOptions { @@ -47,6 +48,7 @@ impl Default for DrawingOptions { batch_item: None, clip: Clip::default(), zeros_as_transparent: false, + global_alpha: 1.0, } } } diff --git a/src/webview-ui/src/components/display_options.rs b/src/webview-ui/src/components/display_options.rs index 6d0970c2..b2635ca1 100644 --- a/src/webview-ui/src/components/display_options.rs +++ b/src/webview-ui/src/components/display_options.rs @@ -1,6 +1,6 @@ use stylist::yew::use_style; use yew::prelude::*; -use yewdux::{prelude::use_selector, Dispatch}; +use yewdux::{use_selector_with_deps, Dispatch}; use crate::{ application_state::{ @@ -105,14 +105,17 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> Html { let drawing_context = *drawing_context; let image_id = entry.image_id.clone(); - let drawing_options = use_selector(move |state: &AppState| { - state - .drawing_options - .borrow() - .get(&image_id, &drawing_context) - .cloned() - .unwrap_or_default() - }); + let drawing_options = use_selector_with_deps( + move |state: &AppState, (image_id, drawing_context)| { + state + .drawing_options + .borrow() + .get(image_id, drawing_context) + .cloned() + .unwrap_or_default() + }, + (image_id, drawing_context), + ); let features = features::list_features(entry); diff --git a/src/webview-ui/src/components/main_toolbar.rs b/src/webview-ui/src/components/main_toolbar.rs index 43673b9f..18888296 100644 --- a/src/webview-ui/src/components/main_toolbar.rs +++ b/src/webview-ui/src/components/main_toolbar.rs @@ -141,12 +141,12 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { overlay.clone(), ); - let cv = use_selector(move |state: &AppState| { - state - .image_views - .borrow() - .get_currently_viewing(overlay.view_id) - }); + let cv = use_selector_with_deps( + move |state: &AppState, view_id: &ViewId| { + state.image_views.borrow().get_currently_viewing(*view_id) + }, + overlay.view_id, + ); let cv_image_id = cv.as_ref().as_ref().map(|cv| cv.id().clone()); if cv_image_id.is_none() { log::warn!( @@ -159,15 +159,27 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { let view_id = overlay.view_id; let expression = use_selector_with_deps( - |state: &AppState, overlay: &OverlayItem| { + |state: &AppState, overlay_id| { let images = state.images.borrow(); let info = images - .get(&overlay.id) - .unwrap_or_else(|| panic!("Image with id {:?} not found", overlay.id)) + .get(overlay_id) + .unwrap_or_else(|| panic!("Image with id {:?} not found", overlay_id)) .minimal(); info.expression.clone() }, - overlay.clone(), + overlay.id.clone(), + ); + + let drawing_options = use_selector_with_deps( + |state: &AppState, (image_id, drawing_context)| { + state + .drawing_options + .borrow() + .get(image_id, drawing_context) + .cloned() + .unwrap_or_default() + }, + (overlay.id.clone(), DrawingContext::Overlay), ); let dispatch = Dispatch::::global(); @@ -210,7 +222,7 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { }; let alpha_state = use_state(|| 1.0); - use_effect_with(overlay.alpha, { + use_effect_with(drawing_options.global_alpha, { let alpha_state = alpha_state.clone(); move |alpha| { alpha_state.set(*alpha); @@ -219,11 +231,10 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { }); let alpha_throttle = { let alpha_state = alpha_state.clone(); - let cv_image_id = cv_image_id.clone(); + let overlay_id = overlay.id.clone(); move || { dispatch.apply(OverlayAction::SetAlpha { - view_id: overlay.view_id, - image_id: cv_image_id.clone(), + image_id: overlay_id.clone(), alpha: *alpha_state, }); } diff --git a/src/webview-ui/src/rendering/image_renderer.rs b/src/webview-ui/src/rendering/image_renderer.rs index bc5e6cf1..d3d975b4 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -545,7 +545,7 @@ impl ImageRenderer { Coloring::Edges => { // for edges, the background color is always black text_color(Vec4::new(0.0, 0.0, 0.0, 1.0), drawing_options) - }, + } Coloring::Heatmap | Coloring::Segmentation => { let name = match drawing_options.coloring { Coloring::Heatmap => &global_drawing_options.heatmap_colormap_name, @@ -645,7 +645,10 @@ impl ImageRenderer { // Overlay specific uniforms uniform_values.insert("u_is_overlay", UniformValue::Bool(&true)); - uniform_values.insert("u_overlay_alpha", UniformValue::Float(&overlay_item.alpha)); + uniform_values.insert( + "u_overlay_alpha", + UniformValue::Float(&drawing_options.global_alpha), + ); uniform_values.insert( "u_zeros_as_transparent", UniformValue::Bool(&drawing_options.zeros_as_transparent), @@ -667,7 +670,7 @@ impl ImageRenderer { if let Some(overlay) = &image_view_data .overlay .as_ref() - .and_then(|o| (!o.hidden && o.alpha > 0.0).then_some(o)) + .and_then(|o| (!o.hidden).then_some(o)) { let texture = rendering_context.texture_by_id(&overlay.id); // log::debug!("Rendering overlay {:?}", overlay); From 0d069df81cd4ae96f01112ccca4d40ecaeffc614 Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:06:29 +0200 Subject: [PATCH 11/18] Enhance Size handling and add size comparison warning in OverlayMenuItem --- src/webview-ui/src/common/types.rs | 8 ++- src/webview-ui/src/components/main_toolbar.rs | 57 +++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/webview-ui/src/common/types.rs b/src/webview-ui/src/common/types.rs index eccd53b7..370eace1 100644 --- a/src/webview-ui/src/common/types.rs +++ b/src/webview-ui/src/common/types.rs @@ -50,7 +50,7 @@ impl CurrentlyViewing { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Copy)] pub(crate) struct Size { pub width: f32, pub height: f32, @@ -65,6 +65,12 @@ impl Size { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct SizeU32 { + pub width: u32, + pub height: u32, +} + #[derive(tsify::Tsify, serde::Deserialize, Debug, Clone, PartialEq)] pub(crate) enum ValueVariableKind { #[serde(rename = "variable")] diff --git a/src/webview-ui/src/components/main_toolbar.rs b/src/webview-ui/src/components/main_toolbar.rs index 18888296..e971ec07 100644 --- a/src/webview-ui/src/components/main_toolbar.rs +++ b/src/webview-ui/src/components/main_toolbar.rs @@ -16,11 +16,8 @@ use crate::{ }, coloring::Coloring, colormap::ColorMapKind, - common::{AppMode, CurrentlyViewing, Image, ViewId}, - components::{ - checkbox::Checkbox, display_options::DisplayOption, icon_button::IconButton, - icon_button::IconButton, - }, + common::{AppMode, CurrentlyViewing, Image, SizeU32, ViewId}, + components::{checkbox::Checkbox, display_options::DisplayOption, icon_button::IconButton}, vscode::vscode_requests::VSCodeRequests, }; @@ -158,7 +155,7 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { let cv_image_id = cv_image_id.unwrap(); let view_id = overlay.view_id; - let expression = use_selector_with_deps( + let overlay_expression = use_selector_with_deps( |state: &AppState, overlay_id| { let images = state.images.borrow(); let info = images @@ -170,6 +167,37 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { overlay.id.clone(), ); + let same_size = use_selector_with_deps( + { + let cv_image_id = cv_image_id.clone(); + move |state: &AppState, overlay_id| { + let images = state.images.borrow(); + let overlay_size = images.get(overlay_id).and_then(|image| { + if let Image::Full(info) = image { + Some(SizeU32 { + width: info.width, + height: info.height, + }) + } else { + None + } + }); + let cv_size = images.get(&cv_image_id).and_then(|image| { + if let Image::Full(info) = image { + Some(SizeU32 { + width: info.width, + height: info.height, + }) + } else { + None + } + }); + overlay_size == cv_size + } + }, + overlay.id.clone(), + ); + let drawing_options = use_selector_with_deps( |state: &AppState, (image_id, drawing_context)| { state @@ -271,6 +299,15 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { }; + let maybe_warning = if !*same_size { + html! { + + } + } else { + html! {} + }; + let style = use_style!( r#" position: relative; @@ -334,8 +371,9 @@ pub fn OverlayMenuItem(props: &OverlayMenuItemProps) -> Html { {show_hide_button} - {expression} + {overlay_expression} + { maybe_warning }
@@ -488,10 +526,7 @@ pub(crate) fn MainToolbar(props: &MainToolbarProps) -> Html { let on_save_click = Callback::from(move |_: MouseEvent| { if let Some(ref image) = *current_image_info_for_save { let minimal = image.minimal(); - VSCodeRequests::save_image( - minimal.image_id.clone(), - minimal.expression.clone(), - ); + VSCodeRequests::save_image(minimal.image_id.clone(), minimal.expression.clone()); } }); From 39152c6f9c310f0872b649fed3c56615cea03afc Mon Sep 17 00:00:00 2001 From: Elazar Date: Sat, 20 Dec 2025 23:06:29 +0200 Subject: [PATCH 12/18] wip --- icons/dist/svifpd-icons.css | 3 +- icons/dist/svifpd-icons.html | 8 + icons/dist/svifpd-icons.svg | 2 +- icons/dist/svifpd-icons.woff2 | Bin 4108 -> 4472 bytes icons/src/icons/overlay.svg | 1 + icons/src/template/mapping.json | 3 +- .../src/components/image_list_item.rs | 158 +++++++++++++----- 7 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 icons/src/icons/overlay.svg diff --git a/icons/dist/svifpd-icons.css b/icons/dist/svifpd-icons.css index 9b075233..45fbac84 100644 --- a/icons/dist/svifpd-icons.css +++ b/icons/dist/svifpd-icons.css @@ -6,7 +6,7 @@ @font-face { font-family: "svifpd-icons"; font-display: block; - src: url("./svifpd-icons.woff2?ddf2b6472b7dddb3c0d7e731d61b8fff") format("woff2"); + src: url("./svifpd-icons.woff2?b34cdd727d5167118abfe162e0202494") format("woff2"); } .svifpd-icons[class*='svifpd-icons-'] { @@ -72,3 +72,4 @@ .svifpd-icons-image:before { content: "\ea6d" } .svifpd-icons-inspect-image:before { content: "\ea6e" } .svifpd-icons-edges:before { content: "\ea6f" } +.svifpd-icons-overlay:before { content: "\ea70" } diff --git a/icons/dist/svifpd-icons.html b/icons/dist/svifpd-icons.html index 35a4bcc6..0909b779 100644 --- a/icons/dist/svifpd-icons.html +++ b/icons/dist/svifpd-icons.html @@ -241,6 +241,14 @@

svifpd-icons

legend
+
+ + + +
+ overlay + +
diff --git a/icons/dist/svifpd-icons.svg b/icons/dist/svifpd-icons.svg index e789c110..94e27b25 100644 --- a/icons/dist/svifpd-icons.svg +++ b/icons/dist/svifpd-icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/dist/svifpd-icons.woff2 b/icons/dist/svifpd-icons.woff2 index 99bc909c751ca811af09b48b8514fbc990926a59..7aca677aeee44d4dac7a989732690909dda748a6 100644 GIT binary patch literal 4472 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 KrPp8p0001T)}y`v literal 4108 zcmV+n5cBVMPew8T0RR9101ylS3jhEB06VMz01vzX0RR9100000000000000000000 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 \ No newline at end of file diff --git a/icons/src/template/mapping.json b/icons/src/template/mapping.json index db283d7b..427a8873 100644 --- a/icons/src/template/mapping.json +++ b/icons/src/template/mapping.json @@ -13,5 +13,6 @@ "tensor": 60012, "image": 60013, "inspect-image": 60014, - "edges": 60015 + "edges": 60015, + "overlay": 60016 } \ No newline at end of file diff --git a/src/webview-ui/src/components/image_list_item.rs b/src/webview-ui/src/components/image_list_item.rs index 0d35e2aa..a5ab2290 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -1,15 +1,15 @@ use itertools::Itertools; use stylist::yew::use_style; use yew::prelude::*; -use yewdux::Dispatch; +use yewdux::{use_selector, Dispatch}; use crate::{ - application_state::{app_state::{AppState, OverlayAction, UiAction}, images::DrawingContext}, - common::{Image, MinimalImageInfo, ValueVariableKind, ViewId}, - components::{ - context_menu::{use_context_menu, ContextMenuData, ContextMenuItem}, - display_options::DisplayOption, + application_state::{ + app_state::{AppState, OverlayAction, UiAction}, + images::DrawingContext, }, + common::{Image, MinimalImageInfo, ValueVariableKind, ViewId}, + components::display_options::DisplayOption, vscode::vscode_requests::VSCodeRequests, }; @@ -137,6 +137,83 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { html!(<>) }; + let is_overlay = use_selector({ + let image_id = image_id.clone(); + move |state: &AppState| { + let overlay = state + .image_views + .borrow() + .get_currently_viewing(ViewId::Primary) + .and_then(|cv| { + state + .overlays + .borrow() + .get_image_overlay(ViewId::Primary, cv.id()) + .map(|overlay| overlay.id.clone()) + }); + overlay.as_ref() == Some(&image_id) + } + }); + let overlay_button = html! { + ::global().get(); + let cv = state.image_views.borrow().get_currently_viewing(view_id); + if let Some(cv) = cv { + Dispatch::::global().apply(OverlayAction::Add { + view_id, + image_id: cv.id().clone(), + overlay_id: image_id.clone(), + }); + } + } + })} + /> + }; + let remove_overlay_style = use_style!( + r#" + background-color: var(--vscode-button-background); + + &:hover { + background-color: var(--vscode-button-hoverBackground); + } + "# + ); + let remove_overlay_button = html! { + ::global().apply(OverlayAction::Remove { + // view_id, + // overlay_id: image_id.clone(), + // }); + } + })} + class={remove_overlay_style} + /> + }; + let set_remove_overlay_button = if *is_overlay { + remove_overlay_button + } else { + overlay_button + }; + let item_style = use_style!( r#" @@ -158,47 +235,48 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { "# ); - let ctx = use_context_menu(); + // let ctx = use_context_menu(); - let on_context = { - let image_id = image_id.clone(); - let ctx = ctx.clone(); - Callback::from(move |e: MouseEvent| { - e.prevent_default(); - ctx.set(Some(ContextMenuData { - x: e.client_x(), - y: e.client_y(), - items: vec![ContextMenuItem { - label: "Overlay".into(), - action: Callback::from({ - let image_id = image_id.clone(); - let ctx = ctx.clone(); - move |_| { - let view_id = ViewId::Primary; - let state = Dispatch::::global().get(); - let cv = state.image_views.borrow().get_currently_viewing(view_id); - if let Some(cv) = cv { - Dispatch::::global().apply(OverlayAction::Add { - view_id, - image_id: cv.id().clone(), - overlay_id: image_id.clone(), - }); - } - ctx.set(None); - } - }), - disabled: false, - }], - })); - }) - }; + // let on_context = { + // let image_id = image_id.clone(); + // let ctx = ctx.clone(); + // Callback::from(move |e: MouseEvent| { + // e.prevent_default(); + // ctx.set(Some(ContextMenuData { + // x: e.client_x(), + // y: e.client_y(), + // items: vec![ContextMenuItem { + // label: "Overlay".into(), + // action: Callback::from({ + // let image_id = image_id.clone(); + // let ctx = ctx.clone(); + // move |_| { + // let view_id = ViewId::Primary; + // let state = Dispatch::::global().get(); + // let cv = state.image_views.borrow().get_currently_viewing(view_id); + // if let Some(cv) = cv { + // Dispatch::::global().apply(OverlayAction::Add { + // view_id, + // image_id: cv.id().clone(), + // overlay_id: image_id.clone(), + // }); + // } + // ctx.set(None); + // } + // }), + // disabled: false, + // }], + // })); + // }) + // }; html! {
+ {set_remove_overlay_button} {pin_unpin_button} if *value_variable_kind == ValueVariableKind::Expression {{edit_button}} else {<>} From 98ae9eb427fa6e090b16c718f164b5f9bdb3288f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:06:29 +0200 Subject: [PATCH 13/18] Initial plan From 57bee12e65bf4d69b0f2a498a04cc6c29fc81938 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:09:01 +0200 Subject: [PATCH 14/18] Complete overlay label feature: add Remove action and set default overlay settings Co-authored-by: elazarcoh <28874499+elazarcoh@users.noreply.github.com> --- .../src/application_state/app_state.rs | 25 +++++++++++++------ .../src/components/image_list_item.rs | 12 ++++++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index 696b1865..4eb86ed9 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -537,6 +537,10 @@ pub(crate) enum OverlayAction { image_id: ViewableObjectId, overlay_id: ViewableObjectId, }, + Remove { + view_id: ViewId, + image_id: ViewableObjectId, + }, Hide { view_id: ViewId, image_id: ViewableObjectId, @@ -567,20 +571,27 @@ impl Reducer for OverlayAction { overlay_id.clone(), ); - // init with 0.8 global alpha + // init with default overlay settings (0.8 alpha and segmentation coloring) if state .drawing_options - .borrow_mut() + .borrow() .get(&overlay_id, &DrawingContext::Overlay) .is_none() { - state - .drawing_options - .borrow_mut() - .get_mut_ref(overlay_id, DrawingContext::Overlay) - .global_alpha = 0.8; + let mut drawing_options = state.drawing_options.borrow_mut(); + let drawing_options_ref = + drawing_options.get_mut_ref(overlay_id, DrawingContext::Overlay); + drawing_options_ref.global_alpha = 0.8; + drawing_options_ref.coloring = Coloring::Segmentation; + drawing_options_ref.zeros_as_transparent = true; } } + OverlayAction::Remove { view_id, image_id } => { + state + .overlays + .borrow_mut() + .clear_overlay(view_id, &image_id); + } OverlayAction::Hide { view_id, image_id: overlay_id, diff --git a/src/webview-ui/src/components/image_list_item.rs b/src/webview-ui/src/components/image_list_item.rs index a5ab2290..ecb30776 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -199,10 +199,14 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { event.prevent_default(); event.stop_propagation(); let view_id = ViewId::Primary; - // Dispatch::::global().apply(OverlayAction::Remove { - // view_id, - // overlay_id: image_id.clone(), - // }); + let state = Dispatch::::global().get(); + let cv = state.image_views.borrow().get_currently_viewing(view_id); + if let Some(cv) = cv { + Dispatch::::global().apply(OverlayAction::Remove { + view_id, + image_id: cv.id().clone(), + }); + } } })} class={remove_overlay_style} From 250647b4f0bdb6b3c18d00cd55123145414c1106 Mon Sep 17 00:00:00 2001 From: Elazar Date: Mon, 22 Dec 2025 19:03:14 +0200 Subject: [PATCH 15/18] Remove debug shader files --- src/webview-ui/debug_shaders/int-image.frag | 166 ---------------- .../debug_shaders/int-planar-image.frag | 183 ------------------ .../debug_shaders/normalized-image.frag | 163 ---------------- .../normalized-planar-image.frag | 182 ----------------- src/webview-ui/debug_shaders/uint-image.frag | 166 ---------------- .../debug_shaders/uint-planar-image.frag | 183 ------------------ 6 files changed, 1043 deletions(-) delete mode 100644 src/webview-ui/debug_shaders/int-image.frag delete mode 100644 src/webview-ui/debug_shaders/int-planar-image.frag delete mode 100644 src/webview-ui/debug_shaders/normalized-image.frag delete mode 100644 src/webview-ui/debug_shaders/normalized-planar-image.frag delete mode 100644 src/webview-ui/debug_shaders/uint-image.frag delete mode 100644 src/webview-ui/debug_shaders/uint-planar-image.frag diff --git a/src/webview-ui/debug_shaders/int-image.frag b/src/webview-ui/debug_shaders/int-image.frag deleted file mode 100644 index 36f05787..00000000 --- a/src/webview-ui/debug_shaders/int-image.frag +++ /dev/null @@ -1,166 +0,0 @@ -#version 300 es - -precision highp float; -precision highp int; -precision highp isampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform isampler2D u_texture; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - -ivec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - diff --git a/src/webview-ui/debug_shaders/int-planar-image.frag b/src/webview-ui/debug_shaders/int-planar-image.frag deleted file mode 100644 index 19a03329..00000000 --- a/src/webview-ui/debug_shaders/int-planar-image.frag +++ /dev/null @@ -1,183 +0,0 @@ -#version 300 es - -precision highp float; -precision highp int; -precision highp isampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform int u_image_type; - -uniform isampler2D u_texture_r; -uniform isampler2D u_texture_g; -uniform isampler2D u_texture_b; -uniform isampler2D u_texture_a; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - diff --git a/src/webview-ui/debug_shaders/normalized-image.frag b/src/webview-ui/debug_shaders/normalized-image.frag deleted file mode 100644 index 0e79cfe1..00000000 --- a/src/webview-ui/debug_shaders/normalized-image.frag +++ /dev/null @@ -1,163 +0,0 @@ -#version 300 es - -precision highp float; -precision highp sampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform sampler2D u_texture; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - -sampled = texture(u_texture, pix); - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - diff --git a/src/webview-ui/debug_shaders/normalized-planar-image.frag b/src/webview-ui/debug_shaders/normalized-planar-image.frag deleted file mode 100644 index f8dfd83a..00000000 --- a/src/webview-ui/debug_shaders/normalized-planar-image.frag +++ /dev/null @@ -1,182 +0,0 @@ -#version 300 es - -precision highp float; -precision highp sampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform int u_image_type; - -uniform sampler2D u_texture_r; -uniform sampler2D u_texture_g; -uniform sampler2D u_texture_b; -uniform sampler2D u_texture_a; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - - - int need = TYPE_TO_NEED[u_image_type]; - if ((need & NEED_RED) != 0) { - sampled.r = texture(u_texture_r, pix).r; - } - if ((need & NEED_GREEN) != 0) { - sampled.g = texture(u_texture_g, pix).r; - } - if ((need & NEED_BLUE) != 0) { - sampled.b = texture(u_texture_b, pix).r; - } - if ((need & NEED_ALPHA) != 0) { - sampled.a = texture(u_texture_a, pix).r; - } - - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - diff --git a/src/webview-ui/debug_shaders/uint-image.frag b/src/webview-ui/debug_shaders/uint-image.frag deleted file mode 100644 index ee9d40b9..00000000 --- a/src/webview-ui/debug_shaders/uint-image.frag +++ /dev/null @@ -1,166 +0,0 @@ -#version 300 es - -precision highp float; -precision highp int; -precision highp usampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform usampler2D u_texture; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - -uvec4 texel = texture(u_texture, pix); -sampled = - vec4(float(texel.r), float(texel.g), float(texel.b), float(texel.a)); - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - diff --git a/src/webview-ui/debug_shaders/uint-planar-image.frag b/src/webview-ui/debug_shaders/uint-planar-image.frag deleted file mode 100644 index 7f44c7d3..00000000 --- a/src/webview-ui/debug_shaders/uint-planar-image.frag +++ /dev/null @@ -1,183 +0,0 @@ -#version 300 es - -precision highp float; -precision highp int; -precision highp usampler2D; - - -in vec2 vout_uv; -layout(location = 0) out vec4 fout_color; - - -uniform int u_image_type; - -uniform usampler2D u_texture_r; -uniform usampler2D u_texture_g; -uniform usampler2D u_texture_b; -uniform usampler2D u_texture_a; - - -// drawing options -uniform float u_normalization_factor; -uniform mat4 u_color_multiplier; -uniform vec4 u_color_addition; -uniform bool u_invert; -uniform bool u_clip_min; -uniform bool u_clip_max; -uniform float u_min_clip_value; -uniform float u_max_clip_value; - -// overlay related uniforms -uniform bool u_is_overlay; -uniform float u_overlay_alpha; - -uniform bool u_use_colormap; -uniform sampler2D u_colormap; - -uniform vec2 u_buffer_dimension; -uniform bool u_enable_borders; - -const float CHECKER_SIZE = 10.0; -const float WHITE_CHECKER = 0.9; -const float BLACK_CHECKER = 0.6; - - - -const int NEED_RED = 1; -const int NEED_GREEN = 2; -const int NEED_BLUE = 4; -const int NEED_ALPHA = 8; - -const int IMAGE_TYPE_GRAYSCALE = 0; -const int IMAGE_TYPE_RGB = 1; -const int IMAGE_TYPE_RGBA = 2; -const int IMAGE_TYPE_GA = 3; - -const int TYPE_TO_NEED[4] = int[]( - NEED_RED, - NEED_RED | NEED_GREEN | NEED_BLUE, - NEED_RED | NEED_GREEN | NEED_BLUE | NEED_ALPHA, - NEED_RED | NEED_GREEN -); - - -float checkboard(vec2 st) { - vec2 pos = mod(st, CHECKER_SIZE * 2.0); - float value = mod(step(CHECKER_SIZE, pos.x) + step(CHECKER_SIZE, pos.y), 2.0); - return mix(BLACK_CHECKER, WHITE_CHECKER, value); -} - -bool is_nan(float val) { - return (val < 0. || 0. < val || val == 0.) ? false : true; -} - - - - -void main() { - vec2 pix = vout_uv; - - vec4 sampled = vec4(0., 0., 0., 1.); - { - - -int need = TYPE_TO_NEED[u_image_type]; -if ((need & NEED_RED) != 0) { - sampled.r = float(texture(u_texture_r, pix).r); -} -if ((need & NEED_GREEN) != 0) { - sampled.g = float(texture(u_texture_g, pix).r); -} -if ((need & NEED_BLUE) != 0) { - sampled.b = float(texture(u_texture_b, pix).r); -} -if ((need & NEED_ALPHA) != 0) { - sampled.a = float(texture(u_texture_a, pix).r); -} - - - } - - vec4 color; - if ( - is_nan(sampled.r) || - is_nan(sampled.g) || - is_nan(sampled.b) || - is_nan(sampled.a) - ) { - - color = vec4(0., 0., 0., 1.); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - } else { - if (u_clip_min) { - sampled = vec4(max(sampled.r, u_min_clip_value), - max(sampled.g, u_min_clip_value), - max(sampled.b, u_min_clip_value), sampled.a); - } - if (u_clip_max) { - sampled = vec4(min(sampled.r, u_max_clip_value), - min(sampled.g, u_max_clip_value), - min(sampled.b, u_max_clip_value), sampled.a); - } - - color = u_color_multiplier * (sampled / u_normalization_factor) + - u_color_addition; - - color = clamp(color, 0.0, 1.0); - - if (u_invert) { - color.rgb = 1. - color.rgb; - } - - if (u_use_colormap) { - vec2 colormap_uv = vec2(color.r, 0.5); - vec4 colormap_color = texture(u_colormap, colormap_uv); - color.rgb = colormap_color.rgb; - } - } - - if (!u_is_overlay) { - float c = checkboard(gl_FragCoord.xy); - color.rgb = mix(vec3(c, c, c), color.rgb, color.a); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - if (u_enable_borders) { - // in case of overlay, we discard this fragment for pixels that are on the border - if (u_is_overlay) { - bool is_border = ( - buffer_position.x < 1.0 || - buffer_position.x > u_buffer_dimension.x - 1.0 || - buffer_position.y < 1.0 || - buffer_position.y > u_buffer_dimension.y - 1.0 - ); - if (is_border) { - discard; - } - } - - float alpha = - max(abs(dFdx(buffer_position.x)), abs(dFdx(buffer_position.y))); - float x_ = fract(buffer_position.x); - float y_ = fract(buffer_position.y); - float vertical_border = - clamp(abs(-1. / alpha * x_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - float horizontal_border = - clamp(abs(-1. / alpha * y_ + .5 / alpha) - (.5 / alpha - 1.), 0., 1.); - color.rgb += vec3(vertical_border + horizontal_border); - } - - if (u_is_overlay) { - color.a = u_overlay_alpha; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - From b3f2c827b5083c9094384b2ecd5b581ba73b4789 Mon Sep 17 00:00:00 2001 From: Elazar Date: Mon, 22 Dec 2025 19:04:05 +0200 Subject: [PATCH 16/18] ensure reflect metadata is top import --- src/extension.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 28612243..32afd694 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,3 +1,5 @@ +/* eslint-disable perfectionist/sort-imports */ +import 'reflect-metadata'; import Container from 'typedi'; import * as vscode from 'vscode'; import { AllViewables } from './AllViewables'; @@ -26,7 +28,6 @@ import { PlotlyFigure, PyplotAxes, PyplotFigure } from './viewable/Plot'; import { NumpyTensor, TorchTensor } from './viewable/Tensor'; import { WebviewRequests } from './webview/communication/createMessages'; import { GlobalWebviewClient } from './webview/communication/WebviewClient'; -import 'reflect-metadata'; function onConfigChange(): void { initLog(); From 9934c79831f15961d11113b92188bc28cf24e1ab Mon Sep 17 00:00:00 2001 From: Elazar Date: Mon, 22 Dec 2025 19:54:16 +0200 Subject: [PATCH 17/18] Complete overlay label feature: update changelog, enhance README, and bump version to 4.1.0 --- CHANGELOG.md | 6 ++++++ README.md | 8 ++++++++ package.json | 2 +- readme-assets/overlay-edge-example.png | Bin 0 -> 38446 bytes readme-assets/overlay-fill-example.png | Bin 0 -> 39171 bytes 5 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 readme-assets/overlay-edge-example.png create mode 100644 readme-assets/overlay-fill-example.png diff --git a/CHANGELOG.md b/CHANGELOG.md index d0dcf495..5f1b28d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [4.1.0] - 2025-12-22 + +### Added +- Edges mode (colorize like segmentation but only show edges). +- Overlay one image on another. + ## [4.0.19] - Pre-release ### Added diff --git a/README.md b/README.md index 5f5e26b0..3a0331f1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ A built-in, enhanced image viewer with the following capabilities: Segmentation

View label images with color mapping (e.g., 0=black, 1=red, etc.).

+
+

Overlay

+
+ Overlay example 1 + Overlay example 2 +
+

Overlay one image on another

+
### Plot Viewer diff --git a/package.json b/package.json index 5996a12b..c9bc302e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "publisher": "elazarcoh", "name": "simply-view-image-for-python-debugging", "displayName": "View Image for Python Debugging", - "version": "4.0.19", + "version": "4.1.0", "packageManager": "yarn@4.4.1", "description": "simply view the image of the image variables when debugging python", "repository": { diff --git a/readme-assets/overlay-edge-example.png b/readme-assets/overlay-edge-example.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f80f748d8e557f75abb96c9d087977e132966e GIT binary patch literal 38446 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�MvZg0E=d?p%DQXiPtJRzKKd@Q{tkN6SZJ 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 ajG*X>y)zUz^P&a(w=yX7v!dluqW=TOx Date: Tue, 23 Dec 2025 11:42:04 +0200 Subject: [PATCH 18/18] Fix changelog date format for version 4.1.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1b28d3..67351bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [4.1.0] - 2025-12-22 +## [4.1.0] - Pre-release ### Added - Edges mode (colorize like segmentation but only show edges).