Skip to content

Commit 65fc04a

Browse files
committed
Add area-probe drag overlay and stats
Implement area-probe rectangle dragging and per-rectangle statistics and UI. Adds viewer state (drag active, start/end UVs, lines) and new APIs (reset_area_probe_overlay, update_area_probe_overlay) plus helper functions to compute rectangle stats and build placeholder/result text lines. Integrates drag behavior into image view input handling (start/update/stop drag, draw translucent selection rect, suppress pan/zoom while probing), resets area probe on image load/clear, and exposes area_probe_lines in the test-engine JSON output. Also adds a test-run flag to force the drag overlay during automation.
1 parent 529557a commit 65fc04a

7 files changed

Lines changed: 247 additions & 40 deletions

File tree

src/imiv/imiv_actions.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ namespace {
3939
reset_view_navigation_state(viewer);
4040
viewer.probe_valid = false;
4141
viewer.probe_channels.clear();
42+
reset_area_probe_overlay(viewer);
4243
}
4344

4445
} // namespace
@@ -145,6 +146,7 @@ load_viewer_image(VulkanState& vk_state, ViewerState& viewer,
145146
reset_view_navigation_state(viewer);
146147
viewer.probe_valid = false;
147148
viewer.probe_channels.clear();
149+
reset_area_probe_overlay(viewer);
148150
if (viewer.image.width > 0 && viewer.image.height > 0) {
149151
const int center_x = viewer.image.width / 2;
150152
const int center_y = viewer.image.height / 2;

src/imiv/imiv_frame.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,27 @@ write_test_engine_viewer_state_json(const std::filesystem::path& out_path,
172172
test_engine_json_write_vec2(f, viewer.max_scroll);
173173
std::fputs(",\n \"fit_image_to_window\": ", f);
174174
std::fputs(ui_state.fit_image_to_window ? "true" : "false", f);
175+
std::fputs(",\n \"loaded_image_count\": ", f);
176+
std::fprintf(f, "%d", static_cast<int>(viewer.loaded_image_paths.size()));
177+
std::fputs(",\n \"current_image_index\": ", f);
178+
std::fprintf(f, "%d", viewer.current_path_index);
179+
std::fputs(",\n \"drag_overlay_active\": ", f);
180+
std::fputs(viewer.drag_overlay_active ? "true" : "false", f);
181+
std::fputs(",\n \"area_probe_drag_active\": ", f);
182+
std::fputs(viewer.area_probe_drag_active ? "true" : "false", f);
175183
std::fputs(",\n \"image_size\": [", f);
176184
std::fprintf(f, "%d,%d", viewer.image.width, viewer.image.height);
177185
std::fputs("],\n \"display_size\": [", f);
178186
std::fprintf(f, "%d,%d", display_width, display_height);
179187
std::fputs("],\n \"orientation\": ", f);
180188
std::fprintf(f, "%d", viewer.image.orientation);
189+
std::fputs(",\n \"area_probe_lines\": [", f);
190+
for (size_t i = 0; i < viewer.area_probe_lines.size(); ++i) {
191+
if (i > 0)
192+
std::fputs(", ", f);
193+
test_engine_json_write_escaped(f, viewer.area_probe_lines[i].c_str());
194+
}
195+
std::fputs("]", f);
181196
std::fputs("\n}\n", f);
182197
std::fflush(f);
183198
std::fclose(f);
@@ -367,6 +382,8 @@ draw_viewer_ui(ViewerState& viewer, PlaceholderUiState& ui_state,
367382
ui_state.show_pixelview_window = true;
368383
ui_state.show_area_probe_window = true;
369384
}
385+
if (env_flag_is_truthy("IMIV_IMGUI_TEST_ENGINE_SHOW_DRAG_OVERLAY"))
386+
viewer.drag_overlay_active = true;
370387

371388
collect_viewer_shortcuts(viewer, ui_state, actions, request_exit);
372389
#if defined(IMGUI_ENABLE_TEST_ENGINE)

src/imiv/imiv_image_view.cpp

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
171171

172172
const bool has_image = !viewer.image.path.empty();
173173
if (has_image) {
174+
if (ui_state.show_area_probe_window && viewer.area_probe_lines.empty())
175+
reset_area_probe_overlay(viewer);
176+
if (!ui_state.show_area_probe_window)
177+
viewer.area_probe_drag_active = false;
178+
174179
const ImVec2 image_size = image_layout.image_size;
175180
ImTextureRef main_texture_ref;
176181
ImTextureRef closeup_texture_ref;
@@ -307,11 +312,12 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
307312
}
308313
}
309314

310-
const ImGuiIO& io = ImGui::GetIO();
311-
const ImVec2 mouse = io.MousePos;
312-
const bool mouse_in_image = point_in_rect(mouse,
313-
coord_map.image_rect_min,
314-
coord_map.image_rect_max);
315+
const ImGuiIO& io = ImGui::GetIO();
316+
const ImVec2 mouse = io.MousePos;
317+
const bool area_probe_mode = ui_state.show_area_probe_window;
318+
const bool mouse_in_image = point_in_rect(mouse,
319+
coord_map.image_rect_min,
320+
coord_map.image_rect_max);
315321
const bool mouse_in_viewport
316322
= point_in_rect(mouse, coord_map.viewport_rect_min,
317323
coord_map.viewport_rect_max);
@@ -331,6 +337,9 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
331337
const bool empty_viewport_clicked_right
332338
= viewport_accepts_mouse && !mouse_in_image
333339
&& ImGui::IsMouseClicked(ImGuiMouseButton_Right);
340+
const ImVec2 clamped_mouse
341+
= clamp_pos_to_rect(mouse, coord_map.image_rect_min,
342+
coord_map.image_rect_max);
334343

335344
ImVec2 source_uv(0.0f, 0.0f);
336345
int px = 0;
@@ -350,12 +359,48 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
350359
viewer.probe_channels.clear();
351360
}
352361

362+
if (area_probe_mode) {
363+
ImVec2 area_source_uv(0.5f, 0.5f);
364+
const bool have_area_source_uv
365+
= screen_to_source_uv(coord_map, clamped_mouse, area_source_uv);
366+
const bool area_probe_accepts_mouse
367+
= viewport_accepts_mouse || image_canvas_accepts_mouse
368+
|| viewer.area_probe_drag_active;
369+
if (!viewer.area_probe_drag_active && area_probe_accepts_mouse
370+
&& ImGui::IsMouseDown(ImGuiMouseButton_Left)
371+
&& have_area_source_uv) {
372+
viewer.area_probe_drag_active = true;
373+
viewer.area_probe_drag_start_uv = area_source_uv;
374+
viewer.area_probe_drag_end_uv = area_source_uv;
375+
}
376+
if (viewer.area_probe_drag_active && have_area_source_uv)
377+
viewer.area_probe_drag_end_uv = area_source_uv;
378+
if (viewer.area_probe_drag_active) {
379+
int xbegin = 0;
380+
int ybegin = 0;
381+
int xend = 0;
382+
int yend = 0;
383+
if (source_uv_to_pixel(coord_map,
384+
viewer.area_probe_drag_start_uv, xbegin,
385+
ybegin)
386+
&& source_uv_to_pixel(coord_map,
387+
viewer.area_probe_drag_end_uv, xend,
388+
yend)) {
389+
update_area_probe_overlay(viewer, xbegin, ybegin, xend,
390+
yend);
391+
}
392+
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left))
393+
viewer.area_probe_drag_active = false;
394+
}
395+
}
396+
353397
bool want_pan = false;
354398
bool want_zoom_drag = false;
355399
if (viewport_accepts_mouse || image_canvas_accepts_mouse
356400
|| viewer.pan_drag_active || viewer.zoom_drag_active) {
357401
if (ui_state.mouse_mode == 1) {
358-
want_pan = ImGui::IsMouseDown(ImGuiMouseButton_Left)
402+
want_pan = (!area_probe_mode
403+
&& ImGui::IsMouseDown(ImGuiMouseButton_Left))
359404
|| ImGui::IsMouseDown(ImGuiMouseButton_Right)
360405
|| ImGui::IsMouseDown(ImGuiMouseButton_Middle);
361406
} else if (ui_state.mouse_mode == 0) {
@@ -364,7 +409,8 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
364409
&& (viewer.pan_drag_active || image_canvas_accepts_mouse
365410
|| viewport_accepts_mouse);
366411
const bool want_alt_left_pan
367-
= io.KeyAlt && ImGui::IsMouseDown(ImGuiMouseButton_Left)
412+
= !area_probe_mode && io.KeyAlt
413+
&& ImGui::IsMouseDown(ImGuiMouseButton_Left)
368414
&& (viewer.pan_drag_active || image_canvas_accepts_mouse
369415
|| viewport_accepts_mouse);
370416
want_pan = want_middle_pan || want_alt_left_pan;
@@ -373,7 +419,7 @@ draw_image_window_contents(ViewerState& viewer, PlaceholderUiState& ui_state,
373419
&& (viewer.zoom_drag_active
374420
|| image_canvas_accepts_mouse
375421
|| viewport_accepts_mouse);
376-
if (!io.KeyAlt
422+
if (!area_probe_mode && !io.KeyAlt
377423
&& (image_canvas_clicked_left || image_canvas_clicked_right
378424
|| empty_viewport_clicked_left
379425
|| empty_viewport_clicked_right)) {

src/imiv/imiv_overlays.cpp

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,103 @@ namespace {
129129
return true;
130130
}
131131

132+
bool compute_rect_stats(const LoadedImage& image, int xbegin, int ybegin,
133+
int xend, int yend, std::vector<double>& out_min,
134+
std::vector<double>& out_max,
135+
std::vector<double>& out_avg, int& out_samples,
136+
ProbeStatsSemantics semantics
137+
= ProbeStatsSemantics::OIIOFloat)
138+
{
139+
out_min.clear();
140+
out_max.clear();
141+
out_avg.clear();
142+
out_samples = 0;
143+
if (image.width <= 0 || image.height <= 0 || image.nchannels <= 0)
144+
return false;
145+
146+
const int xmin = std::clamp(std::min(xbegin, xend), 0, image.width - 1);
147+
const int xmax = std::clamp(std::max(xbegin, xend), 0, image.width - 1);
148+
const int ymin = std::clamp(std::min(ybegin, yend), 0,
149+
image.height - 1);
150+
const int ymax = std::clamp(std::max(ybegin, yend), 0,
151+
image.height - 1);
152+
if (xmax < xmin || ymax < ymin)
153+
return false;
154+
155+
const size_t channels = static_cast<size_t>(image.nchannels);
156+
out_min.assign(channels, std::numeric_limits<double>::infinity());
157+
out_max.assign(channels, -std::numeric_limits<double>::infinity());
158+
out_avg.assign(channels, 0.0);
159+
160+
std::vector<double> sample;
161+
for (int y = ymin; y <= ymax; ++y) {
162+
for (int x = xmin; x <= xmax; ++x) {
163+
if (!sample_loaded_pixel_with_semantics(image, x, y, semantics,
164+
sample)) {
165+
continue;
166+
}
167+
if (sample.size() != channels)
168+
continue;
169+
for (size_t c = 0; c < channels; ++c) {
170+
out_min[c] = std::min(out_min[c], sample[c]);
171+
out_max[c] = std::max(out_max[c], sample[c]);
172+
out_avg[c] += sample[c];
173+
}
174+
++out_samples;
175+
}
176+
}
177+
178+
if (out_samples <= 0) {
179+
out_min.clear();
180+
out_max.clear();
181+
out_avg.clear();
182+
return false;
183+
}
184+
for (double& value : out_avg)
185+
value /= static_cast<double>(out_samples);
186+
return true;
187+
}
188+
189+
void build_area_probe_placeholder_lines(const LoadedImage& image,
190+
std::vector<std::string>& out_lines)
191+
{
192+
out_lines.clear();
193+
out_lines.emplace_back("Area Probe:");
194+
if (image.width <= 0 || image.height <= 0 || image.nchannels <= 0)
195+
return;
196+
for (int c = 0; c < image.nchannels; ++c) {
197+
const std::string channel
198+
= pixel_preview_channel_label(image, static_cast<int>(c));
199+
out_lines.emplace_back(Strutil::fmt::format(
200+
"{:<5}: [min: ----- max: ----- avg: -----]", channel));
201+
}
202+
}
203+
204+
void build_area_probe_result_lines(const LoadedImage& image,
205+
const std::vector<double>& min_values,
206+
const std::vector<double>& max_values,
207+
const std::vector<double>& avg_values,
208+
std::vector<std::string>& out_lines)
209+
{
210+
out_lines.clear();
211+
out_lines.emplace_back("Area Probe:");
212+
const int channel_count = std::max(1, image.nchannels);
213+
for (int c = 0; c < channel_count; ++c) {
214+
const std::string channel
215+
= pixel_preview_channel_label(image, static_cast<int>(c));
216+
if (static_cast<size_t>(c) < min_values.size()
217+
&& static_cast<size_t>(c) < max_values.size()
218+
&& static_cast<size_t>(c) < avg_values.size()) {
219+
out_lines.emplace_back(Strutil::fmt::format(
220+
"{:<5}: [min: {:>6.3f} max: {:>6.3f} avg: {:>6.3f}]",
221+
channel, min_values[c], max_values[c], avg_values[c]));
222+
} else {
223+
out_lines.emplace_back(Strutil::fmt::format(
224+
"{:<5}: [min: ----- max: ----- avg: -----]", channel));
225+
}
226+
}
227+
}
228+
132229
std::string format_probe_iv_float(double value)
133230
{
134231
if (value < 10.0)
@@ -358,6 +455,40 @@ namespace {
358455

359456
} // namespace
360457

458+
void
459+
reset_area_probe_overlay(ViewerState& viewer)
460+
{
461+
viewer.area_probe_drag_active = false;
462+
viewer.area_probe_drag_start_uv = ImVec2(0.0f, 0.0f);
463+
viewer.area_probe_drag_end_uv = ImVec2(0.0f, 0.0f);
464+
build_area_probe_placeholder_lines(viewer.image, viewer.area_probe_lines);
465+
}
466+
467+
void
468+
update_area_probe_overlay(ViewerState& viewer, int xbegin, int ybegin, int xend,
469+
int yend)
470+
{
471+
if (viewer.image.path.empty()) {
472+
viewer.area_probe_lines.clear();
473+
return;
474+
}
475+
476+
std::vector<double> min_values;
477+
std::vector<double> max_values;
478+
std::vector<double> avg_values;
479+
int sample_count = 0;
480+
if (!compute_rect_stats(viewer.image, xbegin, ybegin, xend, yend,
481+
min_values, max_values, avg_values, sample_count,
482+
ProbeStatsSemantics::OIIOFloat)) {
483+
build_area_probe_placeholder_lines(viewer.image,
484+
viewer.area_probe_lines);
485+
return;
486+
}
487+
488+
build_area_probe_result_lines(viewer.image, min_values, max_values,
489+
avg_values, viewer.area_probe_lines);
490+
}
491+
361492
OverlayPanelRect
362493
draw_pixel_closeup_overlay(const ViewerState& viewer,
363494
PlaceholderUiState& ui_state,
@@ -697,38 +828,35 @@ draw_area_probe_overlay(const ViewerState& viewer,
697828
if (!ui_state.show_area_probe_window || !map.valid)
698829
return;
699830

700-
std::vector<std::string> lines;
701-
lines.emplace_back("Area Probe:");
702-
if (viewer.image.path.empty()) {
703-
lines.emplace_back("No image loaded.");
704-
} else {
705-
std::vector<double> min_values;
706-
std::vector<double> max_values;
707-
std::vector<double> avg_values;
708-
int sample_count = 0;
709-
const bool have_stats
710-
= viewer.probe_valid
711-
&& compute_area_stats(viewer.image, viewer.probe_x,
712-
viewer.probe_y, ui_state.closeup_avg_pixels,
713-
min_values, max_values, avg_values,
714-
sample_count,
715-
ProbeStatsSemantics::OIIOFloat);
716-
717-
const int channel_count = std::max(1, viewer.image.nchannels);
718-
for (int c = 0; c < channel_count; ++c) {
719-
const std::string channel
720-
= pixel_preview_channel_label(viewer.image,
721-
static_cast<int>(c));
722-
if (have_stats && static_cast<size_t>(c) < min_values.size()
723-
&& static_cast<size_t>(c) < max_values.size()
724-
&& static_cast<size_t>(c) < avg_values.size()) {
725-
lines.emplace_back(Strutil::fmt::format(
726-
"{:<5}: [min: {:>6.3f} max: {:>6.3f} avg: {:>6.3f}]",
727-
channel, min_values[c], max_values[c], avg_values[c]));
728-
} else {
729-
lines.emplace_back(Strutil::fmt::format(
730-
"{:<5}: [min: ----- max: ----- avg: -----]", channel));
731-
}
831+
if (viewer.area_probe_drag_active) {
832+
ImVec2 start_screen(0.0f, 0.0f);
833+
ImVec2 end_screen(0.0f, 0.0f);
834+
if (source_uv_to_screen(map, viewer.area_probe_drag_start_uv,
835+
start_screen)
836+
&& source_uv_to_screen(map, viewer.area_probe_drag_end_uv,
837+
end_screen)) {
838+
const ImVec2 rect_min(std::min(start_screen.x, end_screen.x),
839+
std::min(start_screen.y, end_screen.y));
840+
const ImVec2 rect_max(std::max(start_screen.x, end_screen.x),
841+
std::max(start_screen.y, end_screen.y));
842+
ImDrawList* draw_list = ImGui::GetWindowDrawList();
843+
draw_list->PushClipRect(map.viewport_rect_min,
844+
map.viewport_rect_max, true);
845+
draw_list->AddRectFilled(rect_min, rect_max,
846+
IM_COL32(52, 128, 255, 76), 0.0f);
847+
draw_list->AddRect(rect_min, rect_max, IM_COL32(96, 176, 255, 220),
848+
0.0f, 0, 1.2f);
849+
draw_list->PopClipRect();
850+
}
851+
}
852+
853+
std::vector<std::string> lines = viewer.area_probe_lines;
854+
if (lines.empty()) {
855+
if (viewer.image.path.empty()) {
856+
lines.emplace_back("Area Probe:");
857+
lines.emplace_back("No image loaded.");
858+
} else {
859+
build_area_probe_placeholder_lines(viewer.image, lines);
732860
}
733861
}
734862

src/imiv/imiv_ui.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ bool
2929
sample_loaded_pixel(const LoadedImage& image, int x, int y,
3030
std::vector<double>& out_channels);
3131
void
32+
reset_area_probe_overlay(ViewerState& viewer);
33+
void
34+
update_area_probe_overlay(ViewerState& viewer, int xbegin, int ybegin, int xend,
35+
int yend);
36+
void
3237
draw_padded_message(const char* message, float x_pad = 10.0f,
3338
float y_pad = 6.0f);
3439
void

src/imiv/imiv_viewer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ struct ViewerState {
4444
int probe_x = 0;
4545
int probe_y = 0;
4646
std::vector<double> probe_channels;
47+
bool area_probe_drag_active = false;
48+
ImVec2 area_probe_drag_start_uv = ImVec2(0.0f, 0.0f);
49+
ImVec2 area_probe_drag_end_uv = ImVec2(0.0f, 0.0f);
50+
std::vector<std::string> area_probe_lines;
4751
bool pan_drag_active = false;
4852
bool zoom_drag_active = false;
4953
ImVec2 drag_prev_mouse = ImVec2(0.0f, 0.0f);

0 commit comments

Comments
 (0)