add basic markers to the sides of a channel view

This commit is contained in:
Rokas Puzonas 2025-03-02 19:56:38 +02:00
parent e33ab321d0
commit a6a66d99fd
4 changed files with 342 additions and 195 deletions

View File

@ -8,9 +8,10 @@ const Graph = @import("./graph.zig");
const NIDaq = @import("ni-daq/root.zig");
const rect_utils = @import("./rect-utils.zig");
const TaskPool = @import("ni-daq/task-pool.zig");
const utils = @import("./utils.zig");
const remap = @import("./utils.zig").remap;
const lerpColor = @import("./utils.zig").lerpColor;
const remap = utils.remap;
const lerpColor = utils.lerpColor;
const log = std.log.scoped(.app);
const assert = std.debug.assert;
const clamp = std.math.clamp;
@ -53,7 +54,7 @@ const ChannelView = struct {
view_rect: Graph.ViewOptions,
follow: bool = false,
height: f32 = 200,
height: f32 = 300,
default_from: f32,
default_to: f32,
@ -498,7 +499,7 @@ fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count
));
}
fn showChannelView(self: *App, channel_view: *ChannelView) !void {
fn showChannelViewGraph(self: *App, channel_view: *ChannelView) !void {
const source = self.getChannelSource(channel_view) orelse return;
const samples = source.samples();
source.lockSamples();
@ -506,11 +507,218 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
var channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect;
const graph_box = self.ui.newBoxFromString("Graph");
graph_box.flags.insert(.clickable);
graph_box.flags.insert(.draggable);
graph_box.background = srcery.black;
graph_box.size.x = UI.Size.percent(1, 0);
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
self.ui.pushParent(graph_box);
defer self.ui.popParent();
const graph_rect = graph_box.computedRect();
const signal = self.ui.signalFromBox(graph_box);
var axis = UI.Axis.X;
var zooming: bool = false;
var start_sample: ?f64 = null;
var stop_sample: ?f64 = null;
if (signal.hot) {
if (signal.shift_modifier) {
axis = UI.Axis.Y;
} else {
axis = UI.Axis.X;
}
if (self.graph_start_sample) |graph_start_sample| {
axis = graph_start_sample.axis;
zooming = true;
}
var mouse_sample: f64 = undefined;
if (axis == .X) {
const mouse_sample_index = channel_rect_opts.mapSampleXToIndex(0, graph_rect.width, signal.relative_mouse.x);
mouse_sample = mouse_sample_index;
} else if (axis == .Y) {
const mouse_sample_value = channel_rect_opts.mapSampleYToValue(0, graph_rect.height, signal.relative_mouse.y);
mouse_sample = mouse_sample_value;
}
start_sample = mouse_sample;
if (signal.flags.contains(.right_pressed)) {
self.graph_start_sample = .{
.value = mouse_sample,
.axis = axis
};
}
if (self.graph_start_sample) |graph_start_sample| {
start_sample = graph_start_sample.value;
stop_sample = mouse_sample;
zooming = true;
}
}
if (zooming) {
if (axis == .X) {
graph_box.active_cursor = .mouse_cursor_resize_ew;
} else {
graph_box.active_cursor = .mouse_cursor_resize_ns;
}
if (signal.flags.contains(.right_released)) {
self.graph_start_sample = null;
if (start_sample != null and stop_sample != null) {
const lower_sample: f64 = @min(start_sample.?, stop_sample.?);
const higher_sample: f64 = @max(start_sample.?, stop_sample.?);
if (axis == .X) {
if (higher_sample - lower_sample > 1) {
channel_rect_opts.from = @floatCast(lower_sample);
channel_rect_opts.to = @floatCast(higher_sample);
} else {
// TODO: Show error message that selected range is too small
}
} else if (axis == .Y) {
if (higher_sample - lower_sample > 0.001) {
channel_rect_opts.min_value = lower_sample;
channel_rect_opts.max_value = higher_sample;
} else {
// TODO: Show error message that selected range is too small
}
}
}
start_sample = null;
stop_sample = null;
}
if (start_sample != null and stop_sample != null) {
const fill = self.ui.newBox(UI.Key.initNil());
fill.background = srcery.green.alpha(0.5);
if (axis == .X) {
const start_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, start_sample.?);
const stop_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, stop_sample.?);
fill.setFixedRect(.{
.x = @floatCast(@min(start_x, stop_x)),
.y = graph_rect.y,
.width = @floatCast(@abs(start_x - stop_x)),
.height = graph_rect.height
});
} else if (axis == .Y) {
const start_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, start_sample.?);
const stop_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, stop_sample.?);
fill.setFixedRect(.{
.x = graph_rect.x,
.y = @floatCast(@min(start_y, stop_y)),
.width = graph_rect.width,
.height = @floatCast(@abs(start_y - stop_y)),
});
}
}
if (start_sample) |sample| {
const marker = self.ui.newBox(UI.Key.initNil());
marker.background = srcery.green;
if (axis == .X) {
const value = samples[@intFromFloat(sample)];
marker.setFmtText(.text, "{d:0.2} | {d:0.6}", .{sample, value});
marker.setFixedRect(UI.Rect{
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
.y = graph_rect.y,
.width = 1,
.height = graph_rect.height
});
} else if (axis == .Y) {
marker.setFmtText(.text, "{d:0.2}", .{sample});
marker.setFixedRect(UI.Rect{
.x = graph_rect.x,
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
.width = graph_rect.width,
.height = 1
});
}
}
if (stop_sample) |sample| {
const marker = self.ui.newBox(UI.Key.initNil());
marker.background = srcery.green;
marker.setFmtText(.text, "{d:0.2}", .{sample});
if (axis == .X) {
marker.setFixedRect(.{
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
.y = graph_rect.y,
.width = 1,
.height = graph_rect.height
});
} else if (axis == .Y) {
marker.setFixedRect(.{
.x = graph_rect.x,
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
.width = graph_rect.width,
.height = 1
});
}
}
} else {
if (signal.dragged()) {
const middle_mouse_drag = signal.flags.contains(.middle_dragging);
var x_offset: f64 = 0;
var y_offset: f64 = 0;
if (signal.shift_modifier or middle_mouse_drag) {
y_offset = remap(
f64,
0, graph_rect.height,
0, channel_rect_opts.max_value - channel_rect_opts.min_value,
signal.drag.y
);
}
if (!signal.shift_modifier or middle_mouse_drag) {
x_offset = remap(
f64,
0, graph_rect.width,
0, channel_rect_opts.to - channel_rect_opts.from,
signal.drag.x
);
}
channel_rect_opts.from -= @floatCast(x_offset);
channel_rect_opts.to -= @floatCast(x_offset);
channel_rect_opts.max_value += @floatCast(y_offset);
channel_rect_opts.min_value += @floatCast(y_offset);
}
}
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_rect_opts.*, samples);
if (channel_view.view_cache.texture) |texture| {
graph_box.texture = texture.texture;
}
}
fn showChannelView(self: *App, channel_view: *ChannelView) !void {
const source = self.getChannelSource(channel_view) orelse return;
var channel_rect_opts: *Graph.ViewOptions = &channel_view.view_rect;
const channel_box = self.ui.newBoxFromPtr(channel_view);
channel_box.background = rl.Color.blue;
channel_box.layout_axis = .Y;
channel_box.size.x = UI.Size.percent(1, 0);
channel_box.size.y = UI.Size.childrenSum(1);
channel_box.size.y = UI.Size.pixels(channel_view.height, 1);
self.ui.pushParent(channel_box);
defer self.ui.popParent();
@ -589,207 +797,127 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
}
}
// const channel_box = self.ui.newBox(UI.Key.initNil());
// channel_box.layout_axis = .Y;
// channel_box.size.x = UI.Size.percent(1, 0);
// channel_box.size.y = UI.Size.childrenSum(1);
// self.ui.pushParent(channel_box);
// defer self.ui.popParent();
var y_axis_markers: *UI.Box = undefined;
var x_axis_markers: *UI.Box = undefined;
{
const graph_box = self.ui.newBoxFromString("Graph");
graph_box.flags.insert(.clickable);
graph_box.flags.insert(.draggable);
graph_box.background = srcery.black;
graph_box.size.x = UI.Size.percent(1, 0);
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
self.ui.pushParent(graph_box);
const y_axis_container = self.ui.newBox(UI.Key.initNil());
y_axis_container.layout_axis = .X;
y_axis_container.size.x = UI.Size.percent(1, 0);
y_axis_container.size.y = UI.Size.percent(1, 0);
self.ui.pushParent(y_axis_container);
defer self.ui.popParent();
const graph_rect = graph_box.computedRect();
y_axis_markers = self.ui.newBoxFromString("Y axis markers");
y_axis_markers.background = rl.Color.blue;
y_axis_markers.size.x = UI.Size.pixels(64, 1);
y_axis_markers.size.y = UI.Size.percent(1, 0);
const signal = self.ui.signalFromBox(graph_box);
try self.showChannelViewGraph(channel_view);
}
var axis = UI.Axis.X;
var zooming: bool = false;
var start_sample: ?f64 = null;
var stop_sample: ?f64 = null;
if (signal.hot) {
if (signal.shift_modifier) {
axis = UI.Axis.Y;
} else {
axis = UI.Axis.X;
}
{
const horizontal_container = self.ui.newBox(UI.Key.initNil());
horizontal_container.layout_axis = .X;
horizontal_container.size.x = UI.Size.childrenSum(0);
horizontal_container.size.y = UI.Size.childrenSum(1);
self.ui.pushParent(horizontal_container);
defer self.ui.popParent();
if (self.graph_start_sample) |graph_start_sample| {
axis = graph_start_sample.axis;
zooming = true;
}
const fullscreen_button = self.ui.newBoxFromString("Fullscreen");
fullscreen_button.background = rl.Color.red;
fullscreen_button.size.x = UI.Size.pixels(y_axis_markers.computedRect().width, 1);
fullscreen_button.size.y = UI.Size.pixels(32, 1);
var mouse_sample: f64 = undefined;
if (axis == .X) {
const mouse_sample_index = channel_rect_opts.mapSampleXToIndex(0, graph_rect.width, signal.relative_mouse.x);
mouse_sample = mouse_sample_index;
} else if (axis == .Y) {
const mouse_sample_value = channel_rect_opts.mapSampleYToValue(0, graph_rect.height, signal.relative_mouse.y);
mouse_sample = mouse_sample_value;
}
x_axis_markers = self.ui.newBoxFromString("X axis markers");
x_axis_markers.background = rl.Color.pink;
x_axis_markers.size.x = UI.Size.percent(1, 0);
x_axis_markers.size.y = UI.Size.pixels(32, 1);
self.ui.pushParent(x_axis_markers);
defer self.ui.popParent();
}
start_sample = mouse_sample;
{
self.ui.pushParent(y_axis_markers);
defer self.ui.popParent();
if (signal.flags.contains(.right_pressed)) {
self.graph_start_sample = .{
.value = mouse_sample,
.axis = axis
};
}
const y_axis_rect = y_axis_markers.computedRect();
if (self.graph_start_sample) |graph_start_sample| {
start_sample = graph_start_sample.value;
stop_sample = mouse_sample;
zooming = true;
}
}
const min_gap_between_markers = 8;
if (zooming) {
if (axis == .X) {
graph_box.active_cursor = .mouse_cursor_resize_ew;
} else {
graph_box.active_cursor = .mouse_cursor_resize_ns;
}
const y_range = channel_rect_opts.max_value - channel_rect_opts.min_value;
if (signal.flags.contains(.right_released)) {
self.graph_start_sample = null;
var axis_marker_size = min_gap_between_markers / y_axis_rect.height * y_range;
axis_marker_size = @ceil(axis_marker_size);
if (start_sample != null and stop_sample != null) {
const lower_sample: f64 = @min(start_sample.?, stop_sample.?);
const higher_sample: f64 = @max(start_sample.?, stop_sample.?);
var marker = utils.roundNearestUp(f64, @max(channel_rect_opts.min_value, channel_view.default_min_value), axis_marker_size);
while (marker < @min(channel_rect_opts.max_value, channel_view.default_max_value)) : (marker += axis_marker_size) {
const marker_box = self.ui.newBox(UI.Key.initNil());
marker_box.background = rl.Color.yellow;
marker_box.setFixedRect(.{
.width = y_axis_rect.width/5,
.height = 1,
.x = y_axis_rect.x + y_axis_rect.width/5*4,
.y = @floatCast(remap(
f64,
channel_rect_opts.max_value, channel_rect_opts.min_value,
y_axis_rect.y, y_axis_rect.y + y_axis_rect.height,
marker
))
});
if (axis == .X) {
if (higher_sample - lower_sample > 1) {
channel_rect_opts.from = @floatCast(lower_sample);
channel_rect_opts.to = @floatCast(higher_sample);
} else {
// TODO: Show error message that selected range is too small
}
} else if (axis == .Y) {
if (higher_sample - lower_sample > 0.01) {
channel_rect_opts.min_value = lower_sample;
channel_rect_opts.max_value = higher_sample;
} else {
// TODO: Show error message that selected range is too small
}
}
}
start_sample = null;
stop_sample = null;
}
if (start_sample != null and stop_sample != null) {
const fill = self.ui.newBox(UI.Key.initNil());
fill.background = srcery.green.alpha(0.5);
if (axis == .X) {
const start_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, start_sample.?);
const stop_x = channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, stop_sample.?);
fill.setFixedRect(.{
.x = @floatCast(@min(start_x, stop_x)),
.y = graph_rect.y,
.width = @floatCast(@abs(start_x - stop_x)),
.height = graph_rect.height
});
} else if (axis == .Y) {
const start_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, start_sample.?);
const stop_y = channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, stop_sample.?);
fill.setFixedRect(.{
.x = graph_rect.x,
.y = @floatCast(@min(start_y, stop_y)),
.width = graph_rect.width,
.height = @floatCast(@abs(start_y - stop_y)),
});
}
}
if (start_sample) |sample| {
const marker = self.ui.newBox(UI.Key.initNil());
marker.background = srcery.green;
if (axis == .X) {
const value = samples[@intFromFloat(sample)];
marker.setFmtText(.text, "{d:0.2} | {d:0.6}", .{sample, value});
marker.setFixedRect(UI.Rect{
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
.y = graph_rect.y,
.width = 1,
.height = graph_rect.height
});
} else if (axis == .Y) {
marker.setFmtText(.text, "{d:0.2}", .{sample});
marker.setFixedRect(UI.Rect{
.x = graph_rect.x,
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
.width = graph_rect.width,
.height = 1
});
}
}
if (stop_sample) |sample| {
const marker = self.ui.newBox(UI.Key.initNil());
marker.background = srcery.green;
marker.setFmtText(.text, "{d:0.2}", .{sample});
if (axis == .X) {
marker.setFixedRect(.{
.x = @floatCast(channel_rect_opts.mapSampleIndexToX(graph_rect.x, graph_rect.width, sample)),
.y = graph_rect.y,
.width = 1,
.height = graph_rect.height
});
} else if (axis == .Y) {
marker.setFixedRect(.{
.x = graph_rect.x,
.y = @floatCast(channel_rect_opts.mapSampleValueToY(graph_rect.y, graph_rect.height, sample)),
.width = graph_rect.width,
.height = 1
});
}
}
} else {
if (signal.dragged()) {
if (signal.shift_modifier) {
const drag_offset = remap(
f64,
0, graph_rect.height,
0, channel_rect_opts.max_value - channel_rect_opts.min_value,
signal.drag.y
);
channel_rect_opts.max_value += @floatCast(drag_offset);
channel_rect_opts.min_value += @floatCast(drag_offset);
} else {
const drag_offset = remap(
f64,
0, graph_rect.width,
0, channel_rect_opts.to - channel_rect_opts.from,
signal.drag.x
);
channel_rect_opts.from -= @floatCast(drag_offset);
channel_rect_opts.to -= @floatCast(drag_offset);
}
}
}
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_rect_opts.*, samples);
if (channel_view.view_cache.texture) |texture| {
graph_box.texture = texture.texture;
const label_box = self.ui.newBox(UI.Key.initNil());
label_box.setFmtText(.text, "{d:.03}", .{marker});
label_box.size.x = UI.Size.text(0, 1);
label_box.size.y = UI.Size.text(0, 1);
label_box.flags.insert(.text_left_align);
label_box.setFixedRect(.{
.width = y_axis_rect.width,
.height = 1,
.x = y_axis_rect.x,
.y = @floatCast(remap(
f64,
channel_rect_opts.max_value, channel_rect_opts.min_value,
y_axis_rect.y, y_axis_rect.y + y_axis_rect.height,
marker
))
});
}
}
self.showChannelViewSlider(
&channel_view.view_rect,
@floatFromInt(samples.len)
);
{
self.ui.pushParent(x_axis_markers);
defer self.ui.popParent();
const y_axis_rect = x_axis_markers.computedRect();
const axis_marker_size = 100000;
var marker = utils.roundNearestUp(f64, @max(channel_rect_opts.from, channel_view.default_from), axis_marker_size);
while (marker < @min(channel_rect_opts.to, channel_view.default_to)) : (marker += axis_marker_size) {
const marker_box = self.ui.newBox(UI.Key.initNil());
marker_box.background = rl.Color.yellow;
marker_box.setFixedRect(.{
.width = 1,
.height = y_axis_rect.height/2,
.x = @floatCast(remap(
f64,
channel_rect_opts.from, channel_rect_opts.to,
y_axis_rect.x, y_axis_rect.x + y_axis_rect.width,
marker
)),
.y = y_axis_rect.y,
});
}
}
}
fn showChannelsWindow(self: *App) !void {

View File

@ -158,7 +158,7 @@ pub fn main() !void {
if (builtin.mode == .Debug) {
// try app.appendChannelFromDevice("Dev1/ai0");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
}

View File

@ -16,7 +16,7 @@ const clamp = std.math.clamp;
const UI = @This();
const debug = false;
const max_boxes = 512;
const max_boxes = 5120;
const max_events = 256;
const RectFormatted = struct {
@ -176,15 +176,19 @@ pub const Event = union(enum) {
pub const Signal = struct {
pub const Flag = enum {
left_pressed,
middle_pressed,
right_pressed,
left_released,
middle_released,
right_released,
left_clicked,
middle_clicked,
right_clicked,
left_dragging,
middle_dragging,
right_dragging,
scrolled
@ -199,11 +203,11 @@ pub const Signal = struct {
shift_modifier: bool = false,
pub fn clicked(self: Signal) bool {
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked);
return self.flags.contains(.left_clicked) or self.flags.contains(.right_clicked) or self.flags.contains(.middle_clicked);
}
pub fn dragged(self: Signal) bool {
return self.flags.contains(.left_dragging) or self.flags.contains(.right_dragging);
return self.flags.contains(.left_dragging) or self.flags.contains(.right_dragging) or self.flags.contains(.middle_dragging);
}
pub fn scrolled(self: Signal) bool {
@ -215,6 +219,8 @@ pub const Signal = struct {
self.flags.insert(.left_pressed);
} else if (mouse_button == .mouse_button_right) {
self.flags.insert(.right_pressed);
} else if (mouse_button == .mouse_button_middle) {
self.flags.insert(.middle_pressed);
}
}
@ -223,6 +229,8 @@ pub const Signal = struct {
self.flags.insert(.left_released);
} else if (mouse_button == .mouse_button_right) {
self.flags.insert(.right_released);
} else if (mouse_button == .mouse_button_middle) {
self.flags.insert(.middle_released);
}
}
@ -231,6 +239,8 @@ pub const Signal = struct {
self.flags.insert(.left_clicked);
} else if (mouse_button == .mouse_button_right) {
self.flags.insert(.right_clicked);
} else if (mouse_button == .mouse_button_middle) {
self.flags.insert(.middle_clicked);
}
}
@ -239,6 +249,8 @@ pub const Signal = struct {
self.flags.insert(.left_dragging);
} else if (mouse_button == .mouse_button_right) {
self.flags.insert(.right_dragging);
} else if (mouse_button == .mouse_button_middle) {
self.flags.insert(.middle_dragging);
}
}
};
@ -856,8 +868,6 @@ fn drawBox(self: *UI, box: *Box) void {
text_rect.y += (box_rect.height - text_size.y) / 2;
}
// text_rect.y += box.persistent.active * text_rect.height*0.1;
const text_color = utils.shiftColorInHSV(text.color, value_shift);
font.drawText(text.content, .{ .x = text_rect.x, .y = text_rect.y }, text_color);
@ -1387,7 +1397,8 @@ pub fn signalFromBox(self: *UI, box: *Box) Signal {
}
if (draggable and self.mouse_delta.equals(Vec2.zero()) == 0) {
inline for (.{ rl.MouseButton.mouse_button_left, rl.MouseButton.mouse_button_right }) |mouse_button| {
const mouse_buttons = [_]rl.MouseButton{ .mouse_button_left, .mouse_button_right, .mouse_button_middle };
inline for (mouse_buttons) |mouse_button| {
const active_box = self.active_box_keys.get(mouse_button);
if (active_box != null and active_box.?.eql(key)) {

View File

@ -53,3 +53,11 @@ pub fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, v
const t = (value - from_min) / (from_max - from_min);
return std.math.lerp(to_min, to_max, t);
}
pub fn roundNearestUp(comptime T: type, value: T, multiple: T) T {
return @ceil(value / multiple) * multiple;
}
pub fn roundNearestDown(comptime T: type, value: T, multiple: T) T {
return @floor(value / multiple) * multiple;
}