add basic markers to the sides of a channel view
This commit is contained in:
parent
e33ab321d0
commit
a6a66d99fd
330
src/app.zig
330
src/app.zig
@ -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,90 +507,6 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
||||
|
||||
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);
|
||||
self.ui.pushParent(channel_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
{
|
||||
const tools_box = self.ui.newBoxFromString("Graph tools");
|
||||
tools_box.background = rl.Color.gray;
|
||||
tools_box.layout_axis = .X;
|
||||
tools_box.size.x = UI.Size.percent(1, 0);
|
||||
tools_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(tools_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
{
|
||||
const reset_view_button = self.ui.button(.text, "Reset view");
|
||||
reset_view_button.size.y = UI.Size.percent(1, 0);
|
||||
|
||||
if (self.ui.signalFromBox(reset_view_button).clicked()) {
|
||||
channel_rect_opts.from = channel_view.default_from;
|
||||
channel_rect_opts.to = channel_view.default_to;
|
||||
channel_rect_opts.min_value = channel_view.default_min_value;
|
||||
channel_rect_opts.max_value = channel_view.default_max_value;
|
||||
}
|
||||
}
|
||||
|
||||
if (source == .device) {
|
||||
const device_channel = source.device;
|
||||
|
||||
if (self.ni_daq) |*ni_daq| {
|
||||
const record_button = self.ui.button(.text, "Record");
|
||||
record_button.size.y = UI.Size.percent(1, 0);
|
||||
|
||||
if (device_channel.active_task != null) {
|
||||
record_button.setText(.text, "Stop");
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(record_button);
|
||||
if (signal.clicked()) {
|
||||
if (device_channel.active_task) |task| {
|
||||
try task.stop();
|
||||
device_channel.active_task = null;
|
||||
} else {
|
||||
const channel_name = device_channel.name.buffer[0..device_channel.name.len :0];
|
||||
device_channel.active_task = try self.task_pool.launchAIVoltageChannel(
|
||||
ni_daq,
|
||||
&device_channel.mutex,
|
||||
&device_channel.samples,
|
||||
.{
|
||||
.continous = .{ .sample_rate = device_channel.max_sample_rate }
|
||||
},
|
||||
.{
|
||||
.min_value = channel_view.default_min_value,
|
||||
.max_value = channel_view.default_max_value,
|
||||
.units = device_channel.units,
|
||||
.channel = channel_name
|
||||
}
|
||||
);
|
||||
|
||||
channel_view.follow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const follow_button = self.ui.button(.text, "Follow");
|
||||
follow_button.size.y = UI.Size.percent(1, 0);
|
||||
if (channel_view.follow) {
|
||||
follow_button.setText(.text, "Unfollow");
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(follow_button);
|
||||
if (signal.clicked()) {
|
||||
channel_view.follow = !channel_view.follow;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const graph_box = self.ui.newBoxFromString("Graph");
|
||||
graph_box.flags.insert(.clickable);
|
||||
graph_box.flags.insert(.draggable);
|
||||
@ -667,7 +584,7 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
||||
// TODO: Show error message that selected range is too small
|
||||
}
|
||||
} else if (axis == .Y) {
|
||||
if (higher_sample - lower_sample > 0.01) {
|
||||
if (higher_sample - lower_sample > 0.001) {
|
||||
channel_rect_opts.min_value = lower_sample;
|
||||
channel_rect_opts.max_value = higher_sample;
|
||||
} else {
|
||||
@ -756,27 +673,32 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
||||
}
|
||||
} else {
|
||||
if (signal.dragged()) {
|
||||
if (signal.shift_modifier) {
|
||||
const drag_offset = remap(
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
channel_rect_opts.max_value += @floatCast(drag_offset);
|
||||
channel_rect_opts.min_value += @floatCast(drag_offset);
|
||||
} else {
|
||||
const drag_offset = remap(
|
||||
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(drag_offset);
|
||||
channel_rect_opts.to -= @floatCast(drag_offset);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -784,12 +706,218 @@ fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
||||
if (channel_view.view_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.showChannelViewSlider(
|
||||
&channel_view.view_rect,
|
||||
@floatFromInt(samples.len)
|
||||
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.pixels(channel_view.height, 1);
|
||||
self.ui.pushParent(channel_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
{
|
||||
const tools_box = self.ui.newBoxFromString("Graph tools");
|
||||
tools_box.background = rl.Color.gray;
|
||||
tools_box.layout_axis = .X;
|
||||
tools_box.size.x = UI.Size.percent(1, 0);
|
||||
tools_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(tools_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
{
|
||||
const reset_view_button = self.ui.button(.text, "Reset view");
|
||||
reset_view_button.size.y = UI.Size.percent(1, 0);
|
||||
|
||||
if (self.ui.signalFromBox(reset_view_button).clicked()) {
|
||||
channel_rect_opts.from = channel_view.default_from;
|
||||
channel_rect_opts.to = channel_view.default_to;
|
||||
channel_rect_opts.min_value = channel_view.default_min_value;
|
||||
channel_rect_opts.max_value = channel_view.default_max_value;
|
||||
}
|
||||
}
|
||||
|
||||
if (source == .device) {
|
||||
const device_channel = source.device;
|
||||
|
||||
if (self.ni_daq) |*ni_daq| {
|
||||
const record_button = self.ui.button(.text, "Record");
|
||||
record_button.size.y = UI.Size.percent(1, 0);
|
||||
|
||||
if (device_channel.active_task != null) {
|
||||
record_button.setText(.text, "Stop");
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(record_button);
|
||||
if (signal.clicked()) {
|
||||
if (device_channel.active_task) |task| {
|
||||
try task.stop();
|
||||
device_channel.active_task = null;
|
||||
} else {
|
||||
const channel_name = device_channel.name.buffer[0..device_channel.name.len :0];
|
||||
device_channel.active_task = try self.task_pool.launchAIVoltageChannel(
|
||||
ni_daq,
|
||||
&device_channel.mutex,
|
||||
&device_channel.samples,
|
||||
.{
|
||||
.continous = .{ .sample_rate = device_channel.max_sample_rate }
|
||||
},
|
||||
.{
|
||||
.min_value = channel_view.default_min_value,
|
||||
.max_value = channel_view.default_max_value,
|
||||
.units = device_channel.units,
|
||||
.channel = channel_name
|
||||
}
|
||||
);
|
||||
|
||||
channel_view.follow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const follow_button = self.ui.button(.text, "Follow");
|
||||
follow_button.size.y = UI.Size.percent(1, 0);
|
||||
if (channel_view.follow) {
|
||||
follow_button.setText(.text, "Unfollow");
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(follow_button);
|
||||
if (signal.clicked()) {
|
||||
channel_view.follow = !channel_view.follow;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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();
|
||||
|
||||
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);
|
||||
|
||||
try self.showChannelViewGraph(channel_view);
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
{
|
||||
self.ui.pushParent(y_axis_markers);
|
||||
defer self.ui.popParent();
|
||||
|
||||
const y_axis_rect = y_axis_markers.computedRect();
|
||||
|
||||
const min_gap_between_markers = 8;
|
||||
|
||||
const y_range = channel_rect_opts.max_value - channel_rect_opts.min_value;
|
||||
|
||||
var axis_marker_size = min_gap_between_markers / y_axis_rect.height * y_range;
|
||||
axis_marker_size = @ceil(axis_marker_size);
|
||||
|
||||
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
|
||||
))
|
||||
});
|
||||
|
||||
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.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 {
|
||||
|
@ -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");
|
||||
}
|
||||
|
23
src/ui.zig
23
src/ui.zig
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user