diff --git a/build.zig b/build.zig index 23771f8..0aaff40 100644 --- a/build.zig +++ b/build.zig @@ -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\""), diff --git a/src/main.zig b/src/main.zig index 6071e32..f53b482 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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);