upgrade graph drawing

This commit is contained in:
Rokas Puzonas 2024-11-30 21:35:07 +02:00
parent 45f659cdd7
commit 24895afce6
2 changed files with 230 additions and 47 deletions

View File

@ -36,6 +36,7 @@ pub fn build(b: *std.Build) !void {
const resource_file = b.addWriteFiles();
// https://www.ryanliptak.com/blog/zig-is-a-windows-resource-compiler/
// TODO: Generate icon file at build time
exe.addWin32ResourceFile(.{
.file = resource_file.add("daq-view.rc", "IDI_ICON ICON \"./src/assets/icon.ico\""),

View File

@ -14,6 +14,8 @@ const Allocator = std.mem.Allocator;
const FontFace = @import("font-face.zig");
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const Rect = rl.Rectangle;
const clamp = std.math.clamp;
const icon_png = @embedFile("./assets/icon.png");
@ -58,7 +60,7 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
}
}
fn remap(from_min: f32, from_max: f32, to_min: f32, to_max: f32, value: f32) f32 {
fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T {
const t = (value - from_min) / (from_max - from_min);
return std.math.lerp(to_min, to_max, t);
}
@ -107,6 +109,169 @@ pub fn nanoToSeconds(ns: i128) f32 {
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
}
const ViewRectangle = struct {
from: f32, // inclusive
to: f32, // exclusive
min_value: f64,
max_value: f64,
left_aligned: bool = true,
color: rl.Color = rl.Color.red,
dot_size: f32 = 2
};
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64) f64 {
return remap(
f64,
view_rect.from, view_rect.to,
draw_rect.x, draw_rect.x + draw_rect.width,
index
);
}
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewRectangle, sample: f64) f64 {
return remap(
f64,
view_rect.min_value, view_rect.max_value,
@floatCast(draw_rect.y + draw_rect.height), @floatCast(draw_rect.y),
sample
);
}
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewRectangle, index: f64, sample: f64) Vec2 {
return .{
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
};
}
fn clampIndex(value: f32, size: usize) f32 {
const size_f32: f32 = @floatFromInt(size);
return clamp(value, 0, size_f32);
}
fn clampIndexUsize(value: f32, size: usize) usize {
const size_f32: f32 = @floatFromInt(size);
return @intFromFloat(clamp(value, 0, size_f32));
}
fn drawGraph(draw_rect: rl.Rectangle, view_rect: ViewRectangle, samples: []const f64) void {
assert(view_rect.left_aligned); // TODO:
assert(view_rect.to > view_rect.from);
rl.drawRectangleLinesEx(draw_rect, 1, rl.Color.black);
if (view_rect.from > @as(f32, @floatFromInt(samples.len))) return;
if (view_rect.to < 0) return;
const sample_count = view_rect.to - view_rect.from;
const samples_per_column = sample_count / draw_rect.width;
const samples_threshold = 2;
if (samples_per_column >= samples_threshold) {
var i = clampIndex(view_rect.from, samples.len);
while (i < clampIndex(view_rect.to, samples.len)) : (i += samples_per_column) {
const color = view_rect.color;
const from_index = clampIndexUsize(i, samples.len);
const to_index = clampIndexUsize(i+samples_per_column, samples.len);
const column_samples = samples[from_index..to_index];
if (column_samples.len == 0) continue;
var column_min = column_samples[0];
var column_max = column_samples[0];
for (column_samples) |sample| {
column_min = @min(column_min, sample);
column_max = @max(column_max, sample);
}
const x = mapSampleX(draw_rect, view_rect, @floatFromInt(from_index));
const y_min = mapSampleY(draw_rect, view_rect, column_min);
const y_max = mapSampleY(draw_rect, view_rect, column_max);
if (column_samples.len == 1 or @abs(y_max - y_min) < 1) {
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
rl.Color.blue
);
rl.drawLineV(
mapSamplePointToGraph(draw_rect, view_rect, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, view_rect, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
rl.Color.blue
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
color
);
}
}
} else {
rl.beginScissorMode(
@intFromFloat(@round(draw_rect.x)),
@intFromFloat(@round(draw_rect.y)),
@intFromFloat(@round(draw_rect.width)),
@intFromFloat(@round(draw_rect.height))
);
defer rl.endScissorMode();
{
const from_index = clampIndexUsize(@floor(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to) + 1, samples.len);
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, view_rect.color);
}
}
{
const from_index = clampIndexUsize(@ceil(view_rect.from), samples.len);
const to_index = clampIndexUsize(@ceil(view_rect.to), samples.len);
const min_circle_size = 0.5;
const max_circle_size = view_rect.dot_size;
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
circle_size = @min(circle_size, max_circle_size);
for (from_index..to_index) |i| {
const center = mapSamplePointToGraph(draw_rect, view_rect, @floatFromInt(i), samples[i]);
rl.drawCircleV(center, circle_size, view_rect.color);
}
}
}
}
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
try file.seekTo(0);
const byte_count = try file.getEndPos();
assert(byte_count % 8 == 0);
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
errdefer allocator.free(samples);
var i: usize = 0;
var buffer: [4096]u8 = undefined;
while (true) {
const count = try file.readAll(&buffer);
if (count == 0) break;
for (0..@divExact(count, 8)) |j| {
samples[i] = std.mem.bytesToValue(f64, &buffer[(j*8)..][0..8]);
i += 1;
}
}
return samples;
}
pub fn main() !void {
const start_time = std.time.nanoTimestamp();
@ -160,7 +325,15 @@ pub fn main() !void {
});
defer app.deinit();
for (devices) |device| {
const example_samples1_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin", .{});
defer example_samples1_file.close();
const example_samples1 = try readSamplesFromFile(allocator, example_samples1_file);
defer allocator.free(example_samples1);
//for (devices) |device| {
{
const device = "Dev1";
if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
assert(voltage_ranges.len > 0);
@ -177,6 +350,7 @@ pub fn main() !void {
.min_value = min_sample,
.max_value = max_sample,
});
break;
}
}
@ -227,9 +401,13 @@ pub fn main() !void {
.font = rl.getFontDefault()
};
var zoom: f64 = 1.0;
var view_from: f32 = 0;
//var view_width: f32 = @floatCast(sample_rate * 20);
var view_width: f32 = 1400;//@floatFromInt(example_samples1.len);
while (!rl.windowShouldClose()) {
const dt = rl.getFrameTime();
rl.beginDrawing();
defer rl.endDrawing();
@ -247,54 +425,55 @@ pub fn main() !void {
channel_samples.mutex.lock();
for (0.., channel_samples.samples) |channel_index, samples| {
const channel = app.channels.items[channel_index];
const min_sample: f32 = @floatCast(channel.min_sample);
const max_sample: f32 = @floatCast(channel.max_sample);
const max_visible_samples: u32 = @intFromFloat(sample_rate * 20);
var shown_samples = samples.items;
if (shown_samples.len > max_visible_samples) {
shown_samples = samples.items[(samples.items.len-max_visible_samples-1)..(samples.items.len-1)];
}
if (shown_samples.len >= 2) {
const color = channel.color; // rl.Color.alpha(channel.color, 1.5 / @as(f32, @floatFromInt(channels.items.len)));
const samples_per_pixel = max_visible_samples / window_width;
var i: f32 = 0;
while (i < @as(f32, @floatFromInt(shown_samples.len)) - samples_per_pixel) : (i += samples_per_pixel) {
const next_i = i + samples_per_pixel;
var min_slice_sample = shown_samples[@intFromFloat(i)];
var max_slice_sample = shown_samples[@intFromFloat(i)];
for (@intFromFloat(i)..@intFromFloat(next_i)) |sub_i| {
min_slice_sample = @min(min_slice_sample, shown_samples[sub_i]);
max_slice_sample = @max(max_slice_sample, shown_samples[sub_i]);
}
const offset_i: f32 = @floatFromInt(max_visible_samples - shown_samples.len);
const start_pos = Vec2.init(
(offset_i + i) / max_visible_samples * window_width,
remap(min_sample, max_sample, 0, window_height, @as(f32, @floatCast(min_slice_sample)) * @as(f32, @floatCast(zoom)))
);
const end_pos = Vec2.init(
(offset_i + i) / max_visible_samples * window_width,
remap(min_sample, max_sample, 0, window_height, @as(f32, @floatCast(max_slice_sample)) * @as(f32, @floatCast(zoom)))
);
rl.drawLineV(start_pos, end_pos, color);
}
}
drawGraph(
rl.Rectangle{
.x = 20,
.y = 20,
.width = window_width - 40,
.height = window_height - 40
},
.{
.from = view_from,
.to = view_from + view_width,
.min_value = channel.min_sample,
.max_value = channel.max_sample
},
samples.items
);
}
channel_samples.mutex.unlock();
if (rl.isKeyPressedRepeat(rl.KeyboardKey.key_e) or rl.isKeyPressed(rl.KeyboardKey.key_e)) {
zoom *= 1.1;
// drawGraph(
// rl.Rectangle{
// .x = 100,
// .y = 20,
// .width = window_width - 200,
// .height = window_height - 40
// },
// .{
// .from = view_from,
// .to = view_from + view_width,
// .min_value = -1,
// .max_value = 1
// },
// example_samples1
// );
const move_speed = view_width * 0.25;
if (rl.isKeyDown(.key_d)) {
view_from += move_speed * dt;
}
if (rl.isKeyPressedRepeat(rl.KeyboardKey.key_q) or rl.isKeyPressed(rl.KeyboardKey.key_q)) {
zoom *= 0.9;
if (rl.isKeyDown(.key_a)) {
view_from -= move_speed * dt;
}
const zoom_speed = 0.5;
if (rl.isKeyDown(.key_w)) {
view_width *= (1 - zoom_speed * dt);
}
if (rl.isKeyDown(.key_s)) {
view_width *= (1 + zoom_speed * dt);
}
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
@ -310,7 +489,10 @@ pub fn main() !void {
try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "Zoom: {d:.03}", .{zoom}, Vec2.init(10, y), rl.Color.black);
try font_face.drawTextAlloc(allocator, "View from: {d:.03}", .{view_from}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "View width: {d:.03}", .{view_width}, Vec2.init(10, y), rl.Color.black);
y += 10;
try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{app.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black);