diff --git a/CHANGELOG.md b/CHANGELOG.md index d0dcf495..67351bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [4.1.0] - Pre-release + +### 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/icons/dist/svifpd-icons.css b/icons/dist/svifpd-icons.css index 9217a63d..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?797726f429734e5d9e1970c77951bcfd") format("woff2"); + src: url("./svifpd-icons.woff2?b34cdd727d5167118abfe162e0202494") format("woff2"); } .svifpd-icons[class*='svifpd-icons-'] { @@ -71,3 +71,5 @@ .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" } +.svifpd-icons-overlay:before { content: "\ea70" } diff --git a/icons/dist/svifpd-icons.html b/icons/dist/svifpd-icons.html index 7481892c..0909b779 100644 --- a/icons/dist/svifpd-icons.html +++ b/icons/dist/svifpd-icons.html @@ -177,6 +177,14 @@

svifpd-icons

contrast +
+ + + +
+ edges + +
@@ -233,6 +241,14 @@

svifpd-icons

legend
+
+ + + +
+ overlay + +
diff --git a/icons/dist/svifpd-icons.svg b/icons/dist/svifpd-icons.svg index f2bd453d..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 4a02d06e..7aca677a 100644 Binary files a/icons/dist/svifpd-icons.woff2 and b/icons/dist/svifpd-icons.woff2 differ 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/icons/overlay.svg b/icons/src/icons/overlay.svg new file mode 100644 index 00000000..7381dd78 --- /dev/null +++ b/icons/src/icons/overlay.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..427a8873 100644 --- a/icons/src/template/mapping.json +++ b/icons/src/template/mapping.json @@ -12,5 +12,7 @@ "legend": 60011, "tensor": 60012, "image": 60013, - "inspect-image": 60014 + "inspect-image": 60014, + "edges": 60015, + "overlay": 60016 } \ No newline at end of file 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 00000000..c7f80f74 Binary files /dev/null and b/readme-assets/overlay-edge-example.png differ diff --git a/readme-assets/overlay-fill-example.png b/readme-assets/overlay-fill-example.png new file mode 100644 index 00000000..590b081a Binary files /dev/null and b/readme-assets/overlay-fill-example.png differ 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/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(); 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 deleted file mode 100644 index dec6dac3..00000000 --- a/src/webview-ui/debug_shaders/int-image.frag +++ /dev/null @@ -1,311 +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_zeros_as_transparent; -uniform bool u_edges_only; - -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; - -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - - - -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; -} - -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; -} - - - - -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_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); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - 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); - 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 * color.a; - } 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 cdb6db70..00000000 --- a/src/webview-ui/debug_shaders/normalized-image.frag +++ /dev/null @@ -1,290 +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_zeros_as_transparent; -uniform bool u_edges_only; - -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; - -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - - - -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; -} - -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; -} - - - - -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_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); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - 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); - 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 * color.a; - } 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 62340fb6..00000000 --- a/src/webview-ui/debug_shaders/normalized-planar-image.frag +++ /dev/null @@ -1,435 +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_zeros_as_transparent; -uniform bool u_edges_only; - -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; - -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - - - -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; -} - -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; -} - - - - -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_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); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - 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); - 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 * color.a; - } 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 a8a8a797..00000000 --- a/src/webview-ui/debug_shaders/uint-image.frag +++ /dev/null @@ -1,311 +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_zeros_as_transparent; -uniform bool u_edges_only; - -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; - -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - - - -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; -} - -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; -} - - - - -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_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); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - 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); - 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 * color.a; - } 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 7a1d7807..00000000 --- a/src/webview-ui/debug_shaders/uint-planar-image.frag +++ /dev/null @@ -1,436 +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_zeros_as_transparent; -uniform bool u_edges_only; - -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; - -// Thickness of the edge as a fraction of the pixel size -const float EDGE_THICKNESS = 0.2; - - - -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; -} - -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; -} - - - - -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_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); - } - - vec2 buffer_position = vout_uv * u_buffer_dimension; - 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); - 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 * color.a; - } else { - // alpha is always 1.0 after checkboard is mixed in - color.a = 1.0; - } - - fout_color = color; -} - 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/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/debug_shaders/int-planar-image.frag b/src/webview-ui/shaders/src/shader_parts.rs similarity index 66% rename from src/webview-ui/debug_shaders/int-planar-image.frag rename to src/webview-ui/shaders/src/shader_parts.rs index 95691f51..5955269f 100644 --- a/src/webview-ui/debug_shaders/int-planar-image.frag +++ b/src/webview-ui/shaders/src/shader_parts.rs @@ -1,21 +1,22 @@ -#version 300 es - -precision highp float; -precision highp int; -precision highp isampler2D; - +#![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; - -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; - +{TEXTURES} // drawing options uniform float u_normalization_factor; @@ -46,248 +47,87 @@ 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} - -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) { +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) { +bool is_nan(float val) {{ return (val < 0. || 0. < val || val == 0.) ? false : true; -} +}} -bool is_edge(vec2 uv) { +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); -} - - + {SAMPLE_CODE} 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); -} - - + {SAMPLE_CODE} 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); -} - - + {SAMPLE_CODE} 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); -} - - + {SAMPLE_CODE} 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); -} - - + {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.); - - -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); -} - - + {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.); - - -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); -} - - + {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.); - - -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); -} - - + {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.); - - -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); -} - - + {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); @@ -325,34 +165,18 @@ if ((need & NEED_ALPHA) != 0) { 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} -void main() { +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); -} - - - } + {{ + {SAMPLE_CODE} + }} vec4 color; if ( @@ -360,59 +184,59 @@ if ((need & NEED_ALPHA) != 0) { is_nan(sampled.g) || is_nan(sampled.b) || is_nan(sampled.a) - ) { + ) {{ color = vec4(0., 0., 0., 1.); - if (u_invert) { + if (u_invert) {{ color.rgb = 1. - color.rgb; - } + }} - } else { - if (u_clip_min) { + }} 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) { + }} + 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) { + if (u_invert) {{ color.rgb = 1. - color.rgb; - } + }} - if (u_use_colormap) { + 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_edges_only) { - if (!is_edge(vout_uv)) { + 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.) { + if (u_zeros_as_transparent && color.r == 0. && color.g == 0. && color.b == 0.) {{ color.a = 0.0; - } + }} - if (!u_is_overlay) { + 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 && !u_is_overlay) { + 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); @@ -422,15 +246,166 @@ if ((need & NEED_ALPHA) != 0) { 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) { + if (u_is_overlay) {{ color.a = u_overlay_alpha * color.a; - } else { + }} else {{ // 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/app.rs b/src/webview-ui/src/app.rs index 44dfb6f4..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; @@ -16,6 +17,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 +35,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; @@ -64,25 +69,34 @@ fn rendering_context() -> impl RenderingContext { fn view_data(&self, view_id: ViewId) -> ImageViewData { let dispatch = Dispatch::::global(); - 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 + let state = dispatch.get(); + let currently_viewing = state.image_views.borrow().get_currently_viewing(view_id); + let overlay = currently_viewing.as_ref().and_then(|cv| { + state + .overlays .borrow() - .get_currently_viewing(view_id), + .get_image_overlay(view_id, cv.id()) + .cloned() + }); + + 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, + html_element, + currently_viewing, + overlay, } } @@ -94,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) } @@ -139,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 @@ -272,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)); @@ -358,6 +380,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 +420,10 @@ pub(crate) fn App() -> Html { html! {
-
+ +
+ +
} } diff --git a/src/webview-ui/src/application_state/app_state.rs b/src/webview-ui/src/application_state/app_state.rs index cc3e6636..4eb86ed9 100644 --- a/src/webview-ui/src/application_state/app_state.rs +++ b/src/webview-ui/src/application_state/app_state.rs @@ -3,6 +3,8 @@ 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; use crate::common::texture_image::TextureImage; @@ -52,6 +54,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 +78,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(), @@ -127,6 +131,7 @@ impl AppState { } } +#[derive(PartialEq, Clone)] pub(crate) enum UpdateDrawingOptions { Full(DrawingOptions), Reset, @@ -163,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), @@ -226,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); } @@ -256,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()); @@ -275,20 +284,31 @@ 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 @ (Coloring::Segmentation | Coloring::Edges), + ) => DrawingOptions { + coloring, + 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 @@ -325,10 +345,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"); @@ -471,7 +492,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), @@ -485,7 +511,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 @@ -504,3 +530,106 @@ impl Reducer for UiAction { app_state } } + +pub(crate) enum OverlayAction { + Add { + view_id: ViewId, + image_id: ViewableObjectId, + overlay_id: ViewableObjectId, + }, + Remove { + view_id: ViewId, + image_id: ViewableObjectId, + }, + Hide { + view_id: ViewId, + image_id: ViewableObjectId, + }, + Show { + view_id: ViewId, + image_id: ViewableObjectId, + }, + SetAlpha { + image_id: ViewableObjectId, + alpha: f32, + }, +} + +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_to_image( + view_id, + image_id, + overlay_id.clone(), + ); + + // init with default overlay settings (0.8 alpha and segmentation coloring) + if state + .drawing_options + .borrow() + .get(&overlay_id, &DrawingContext::Overlay) + .is_none() + { + 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, + } => { + 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 { 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; + } + } + + app_state + } +} 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 e57601c6..badb7612 100644 --- a/src/webview-ui/src/application_state/views.rs +++ b/src/webview-ui/src/application_state/views.rs @@ -86,3 +86,59 @@ impl Default for ImageViews { Self::new() } } + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct OverlayItem { + pub(crate) view_id: ViewId, + pub(crate) id: ViewableObjectId, + pub(crate) hidden: bool, +} + +impl OverlayItem { + fn new(view_id: ViewId, id: ViewableObjectId) -> Self { + Self { + view_id, + id, + hidden: false, + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct Overlays { + overlays: HashMap<(ViewId, ViewableObjectId), OverlayItem>, +} + +impl Overlays { + pub(crate) fn add_overlay_to_image( + &mut self, + view_id: ViewId, + image_id: ViewableObjectId, + overlay_id: ViewableObjectId, + ) { + self.overlays.insert( + (view_id, image_id.clone()), + OverlayItem::new(view_id, overlay_id), + ); + } + + pub(crate) fn get_image_overlay( + &self, + view_id: ViewId, + image_id: &ViewableObjectId, + ) -> 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_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..507c8b50 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, } @@ -33,6 +34,8 @@ pub(crate) struct DrawingOptions { pub ignore_alpha: bool, pub batch_item: Option, pub clip: Clip, + pub zeros_as_transparent: bool, + pub global_alpha: f32, } impl Default for DrawingOptions { @@ -44,6 +47,8 @@ impl Default for DrawingOptions { ignore_alpha: false, batch_item: None, clip: Clip::default(), + zeros_as_transparent: false, + global_alpha: 1.0, } } } @@ -208,8 +213,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/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/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/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/display_options.rs b/src/webview-ui/src/components/display_options.rs index 09ca372e..b2635ca1 100644 --- a/src/webview-ui/src/components/display_options.rs +++ b/src/webview-ui/src/components/display_options.rs @@ -1,19 +1,22 @@ 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::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 { @@ -52,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) { @@ -62,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, @@ -94,12 +98,24 @@ 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) - }); + 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); @@ -119,6 +135,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 +165,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 +179,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 +191,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 +203,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 +215,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 +227,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 +239,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 +251,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 +263,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 +275,19 @@ 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 edges_button = html! { + }; // let tensor_button = html! { @@ -343,6 +335,7 @@ pub(crate) fn DisplayOption(props: &DisplayOptionProps) -> 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); @@ -383,4 +376,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 53d90641..ecb30776 100644 --- a/src/webview-ui/src/components/image_list_item.rs +++ b/src/webview-ui/src/components/image_list_item.rs @@ -1,11 +1,14 @@ 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, UiAction}, - common::{Image, MinimalImageInfo, ValueVariableKind}, + application_state::{ + app_state::{AppState, OverlayAction, UiAction}, + images::DrawingContext, + }, + common::{Image, MinimalImageInfo, ValueVariableKind, ViewId}, components::display_options::DisplayOption, vscode::vscode_requests::VSCodeRequests, }; @@ -134,6 +137,87 @@ 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().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} + /> + }; + let set_remove_overlay_button = if *is_overlay { + remove_overlay_button + } else { + overlay_button + }; + let item_style = use_style!( r#" @@ -155,9 +239,48 @@ pub(crate) fn ImageListItem(props: &ImageListItemProps) -> Html { "# ); + // 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, + // }], + // })); + // }) + // }; + html! { -
+
+ {set_remove_overlay_button} {pin_unpin_button} if *value_variable_kind == ValueVariableKind::Expression {{edit_button}} else {<>} @@ -168,7 +291,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 92a578fc..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) } }); @@ -106,7 +106,12 @@ fn ImageItemWrapper(props: &ImageItemWrapperProps) -> Html { {onclick} class={entry_style.clone()} > - +
} } 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 9a389f24..e971ec07 100644 --- a/src/webview-ui/src/components/main_toolbar.rs +++ b/src/webview-ui/src/components/main_toolbar.rs @@ -9,10 +9,14 @@ use yew::prelude::*; use yewdux::{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}, + images::DrawingContext, + views::OverlayItem, + }, coloring::Coloring, colormap::ColorMapKind, - common::{AppMode, Image, ViewId}, + common::{AppMode, CurrentlyViewing, Image, SizeU32, ViewId}, components::{checkbox::Checkbox, display_options::DisplayOption, icon_button::IconButton}, vscode::vscode_requests::VSCodeRequests, }; @@ -109,6 +113,281 @@ 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_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!( + "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 overlay_expression = use_selector_with_deps( + |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)) + .minimal(); + info.expression.clone() + }, + 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 + .drawing_options + .borrow() + .get(image_id, drawing_context) + .cloned() + .unwrap_or_default() + }, + (overlay.id.clone(), DrawingContext::Overlay), + ); + + 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(drawing_options.global_alpha, { + let alpha_state = alpha_state.clone(); + move |alpha| { + alpha_state.set(*alpha); + || () + } + }); + let alpha_throttle = { + let alpha_state = alpha_state.clone(); + let overlay_id = overlay.id.clone(); + move || { + dispatch.apply(OverlayAction::SetAlpha { + image_id: overlay_id.clone(), + alpha: *alpha_state, + }); + } + }; + let alpha_slider = html! { + () + .unwrap() + .value(); + if let Ok(value) = value.parse::() { + alpha_state.set(value); + alpha_throttle(); + } + } + }) + } + /> + }; + + let display_options = html! { + + }; + + let maybe_warning = if !*same_size { + html! { + + } + } else { + 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; + 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 { + width: 100px; + margin-left: 10px; + } + "# + ); + + html! { +
+
+ + {show_hide_button} + + + {overlay_expression} + + { maybe_warning } +
+
+
+ { display_options } +
+
+ + { alpha_slider } +
+
+
+ } +} + #[derive(PartialEq, Properties)] pub(crate) struct MainToolbarProps {} @@ -126,7 +405,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(), @@ -134,7 +419,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() @@ -147,6 +432,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 +512,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| { @@ -224,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()); } }); @@ -236,10 +535,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 { } } @@ -251,8 +551,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/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/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/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 5bce314e..d3d975b4 100644 --- a/src/webview-ui/src/rendering/image_renderer.rs +++ b/src/webview-ui/src/rendering/image_renderer.rs @@ -9,6 +9,10 @@ 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; use crate::coloring::{calculate_color_matrix, Coloring, DrawingOptions}; use crate::common::camera; @@ -36,6 +40,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, @@ -164,6 +174,12 @@ impl ImageRenderer { gl.enable(WebGl2RenderingContext::SCISSOR_TEST); + 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(); let placeholder_texture = create_placeholder_texture(&gl).unwrap(); @@ -197,32 +213,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()?; @@ -273,13 +289,12 @@ 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 - .drawing_options(image_id) + .drawing_options(image_id, &DrawingContext::BaseImage) .0 .batch_item .filter(|i| texture.borrow().textures.contains_key(i)); @@ -306,114 +321,37 @@ impl ImageRenderer { Ok(()) } - 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 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, }, - }; - let config = rendering_context.rendering_configuration(); - - 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 enable_borders = - pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_border as _; - 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 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_normalization_factor", - UniformValue::Float(&coloring_factors.normalization_factor), - ), - ( - "u_color_multiplier", - UniformValue::Mat4(&coloring_factors.color_multiplier), - ), - ( - "u_color_addition", - UniformValue::Vec4(&coloring_factors.color_addition), - ), - ("u_invert", UniformValue::Bool(&drawing_options.invert)), - ])); + } + } - let get_textures = |batch_index: u32| match texture.textures[&batch_index] { + 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))]) } @@ -451,128 +389,416 @@ impl ImageRenderer { ("u_texture_b", UniformValue::Texture(blue)), ("u_texture_a", UniformValue::Texture(alpha)), ]), + } + } + + #[allow(clippy::too_many_arguments)] + 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, + drawing_context: &DrawingContext, + uniform_values: &mut HashMap<&'static str, UniformValue<'a>>, + ) { + let texture_info = &texture.info; + let config = rendering_context.rendering_configuration(); + + 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 enable_borders = + pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_border as _; + let image_size = texture.image_size(); + let image_size_vec = Vec2::new(image_size.width, image_size.height); + + 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); + + 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_normalization_factor", + UniformValue::Float_(coloring_factors.normalization_factor), + ), + ( + "u_color_multiplier", + UniformValue::Mat4_(coloring_factors.color_multiplier), + ), + ( + "u_color_addition", + UniformValue::Vec4_(coloring_factors.color_addition), + ), + ("u_invert", UniformValue::Bool_(drawing_options.invert)), + ])); let is_batched = batch_item.is_some(); let batch_index = batch_item.unwrap_or(0); - uniform_values.extend(get_textures(batch_index)); + uniform_values.extend(ImageRenderer::get_texture_uniforms(texture, batch_index)); - 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"); - - let tex = color_map_texture.obj.clone(); - Some(tex) - } 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"); + uniform_values.insert( + "u_edges_only", + UniformValue::Bool_(drawing_options.coloring == Coloring::Edges), + ); - let tex = color_map_texture.obj.clone(); - Some(tex) + 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 { - None - }; + uniform_values.insert("u_use_colormap", UniformValue::Bool(&false)); + uniform_values.insert( + "u_colormap", + UniformValue::Texture(&rendering_data.placeholder_texture), + ); + } if texture_info.channels == Channels::One { - if let Some(ref clip_min) = drawing_options.clip.min { + 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)); + 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 { + 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)); + uniform_values.insert("u_max_clip_value", UniformValue::Float_(clip_max)); } else { uniform_values.insert("u_clip_max", UniformValue::Bool(&false)); } } + } - 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), - ); + #[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); + + 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::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, + 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_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(&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 + .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 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"); + + 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, + &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(&drawing_options.global_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); set_buffers_and_attributes(program, &rendering_data.image_plane_buffer); 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 { - let pixel_text_cache = rendering_data - .pixel_text_cache_per_view - .get_mut(view_name) - .unwrap(); + 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).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, + ); + } + } + } - 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, - )); + 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 pixel = UVec2::new(x as _, y as _); + let gl = &rendering_data.gl; + let program = ImageRenderer::program_for_texture(&texture, &rendering_data.programs); + let config = rendering_context.rendering_configuration(); - let batch_index = if is_batched { batch_index } else { 0 }; + 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 pixel_value = PixelValue::from_image_info( - &texture.info, - &texture.bytes[&batch_index], - &pixel, - ); + 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"); - // 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; + let tex = color_map_texture.obj.clone(); + Some(tex) + } 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"); - text_color(pixel_color, &drawing_options) - } - }; + let tex = color_map_texture.obj.clone(); + Some(tex) + } else { + None + }; - 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, - }); - } - } + let mut uniform_values = HashMap::new(); + + ImageRenderer::prepare_texture_uniforms( + rendering_context, + rendering_data, + &texture, + 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); + 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, + ); + + 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); + + pixels_info.image_pixel_size_device > config.minimum_size_to_render_pixel_values as _ + }; + + if to_render_text { + 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/rendering/rendering_context.rs b/src/webview-ui/src/rendering/rendering_context.rs index 0cd1f8f7..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}, + application_state::{ + app_state::GlobalDrawingOptions, + images::{DrawingContext, ImageAvailability}, + views::OverlayItem, + }, coloring::DrawingOptions, colormap, common::{ @@ -18,6 +22,7 @@ use crate::{ pub(crate) struct ImageViewData { pub html_element: HtmlElement, pub currently_viewing: Option, + pub overlay: Option, pub camera: camera::Camera, } @@ -37,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( diff --git a/src/webview-ui/src/shaders/image-uint.frag b/src/webview-ui/src/shaders/image-uint.frag index f67cdc0a..0ccdee79 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,42 @@ 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, 0.0); + discard; + } + } else { + sampled = vec4(0.2, 0.2, 0.2, 1.0); + } + if (u_clip_min) { sampled = vec4(max(sampled.r, u_min_clip_value), max(sampled.g, u_min_clip_value), 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; 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.