add showing a minimap
This commit is contained in:
parent
801f9f6ef4
commit
160b7778ce
@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void {
|
|||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
exe.addIncludePath(b.path("src"));
|
||||||
|
exe.addCSourceFile(.{
|
||||||
|
.file = b.path("src/cute_aseprite.c")
|
||||||
|
});
|
||||||
exe.linkLibrary(raylib_dep.artifact("raylib"));
|
exe.linkLibrary(raylib_dep.artifact("raylib"));
|
||||||
exe.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
exe.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
||||||
|
|
||||||
|
282
src/app.zig
282
src/app.zig
@ -2,13 +2,15 @@ const std = @import("std");
|
|||||||
const rl = @import("raylib");
|
const rl = @import("raylib");
|
||||||
const TaskPool = @import("./task-pool.zig");
|
const TaskPool = @import("./task-pool.zig");
|
||||||
const FontFace = @import("./font-face.zig");
|
const FontFace = @import("./font-face.zig");
|
||||||
const Graph = @import("./graph.zig");
|
const Graph = @import("./ui/graph.zig");
|
||||||
const Platform = @import("./platform.zig");
|
const Platform = @import("./platform.zig");
|
||||||
const NIDaq = @import("ni-daq.zig");
|
const NIDaq = @import("ni-daq.zig");
|
||||||
const Theme = @import("./theme.zig");
|
const Theme = @import("./theme.zig");
|
||||||
const RectUtils = @import("./rect-utils.zig");
|
const RectUtils = @import("./rect-utils.zig");
|
||||||
const UI = @import("./ui/root.zig");
|
const UI = @import("./ui/root.zig");
|
||||||
const showButton = @import("./ui/button.zig").showButton;
|
const showButton = @import("./ui/button.zig").showButton;
|
||||||
|
const remap = @import("./utils.zig").remap;
|
||||||
|
const Aseprite = @import("./aseprite.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
@ -22,11 +24,13 @@ const Channel = struct {
|
|||||||
view_cache: Graph.Cache = .{},
|
view_cache: Graph.Cache = .{},
|
||||||
view_rect: Graph.ViewOptions,
|
view_rect: Graph.ViewOptions,
|
||||||
|
|
||||||
|
dragged_marker: ?enum { from, to, both } = null,
|
||||||
|
|
||||||
min_value: f64,
|
min_value: f64,
|
||||||
max_value: f64,
|
max_value: f64,
|
||||||
samples: union(enum) {
|
samples: union(enum) {
|
||||||
owned: []f64,
|
owned: []f64,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
@ -44,18 +48,48 @@ view_width: f32 = 5000,
|
|||||||
|
|
||||||
ui: UI,
|
ui: UI,
|
||||||
|
|
||||||
|
grab_texture: struct {
|
||||||
|
normal: rl.Texture2D,
|
||||||
|
hot: rl.Texture2D,
|
||||||
|
active: rl.Texture2D,
|
||||||
|
},
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
task_pool_options: TaskPool.Options,
|
task_pool_options: TaskPool.Options,
|
||||||
nidaq_options: NIDaq.Options
|
nidaq_options: NIDaq.Options
|
||||||
) !App {
|
) !App {
|
||||||
|
|
||||||
|
// TODO: Maybe store a compressed version of aseprite files when embedding?
|
||||||
|
// Setup a build step to compress the files
|
||||||
|
const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase"));
|
||||||
|
defer grab_ase.deinit();
|
||||||
|
|
||||||
|
const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound);
|
||||||
|
defer rl.unloadImage(grab_normal_image);
|
||||||
|
const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image);
|
||||||
|
|
||||||
|
const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound);
|
||||||
|
defer rl.unloadImage(grab_hot_image);
|
||||||
|
const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image);
|
||||||
|
|
||||||
|
const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound);
|
||||||
|
defer rl.unloadImage(grab_active_image);
|
||||||
|
const grab_active_texture = rl.loadTextureFromImage(grab_active_image);
|
||||||
|
|
||||||
return App{
|
return App{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.task_pool = try TaskPool.init(allocator, task_pool_options),
|
.task_pool = try TaskPool.init(allocator, task_pool_options),
|
||||||
.channels = std.ArrayList(Channel).init(allocator),
|
.channels = std.ArrayList(Channel).init(allocator),
|
||||||
.ni_daq = try NIDaq.init(allocator, nidaq_options),
|
.ni_daq = try NIDaq.init(allocator, nidaq_options),
|
||||||
.start_time_ns = std.time.nanoTimestamp(),
|
.start_time_ns = std.time.nanoTimestamp(),
|
||||||
.ui = UI.init()
|
.ui = UI.init(),
|
||||||
|
|
||||||
|
.grab_texture = .{
|
||||||
|
.normal = grab_normal_texture,
|
||||||
|
.hot = grab_hot_texture,
|
||||||
|
.active = grab_active_texture
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,12 +103,11 @@ pub fn deinit(self: *App) void {
|
|||||||
self.channels.deinit();
|
self.channels.deinit();
|
||||||
self.task_pool.deinit(self.allocator);
|
self.task_pool.deinit(self.allocator);
|
||||||
self.ni_daq.deinit(self.allocator);
|
self.ni_daq.deinit(self.allocator);
|
||||||
}
|
|
||||||
|
|
||||||
// fn appendChannel(self: *App) !*Channel {
|
rl.unloadTexture(self.grab_texture.normal);
|
||||||
// try self.channels.append(Channel.init());
|
rl.unloadTexture(self.grab_texture.hot);
|
||||||
// return &self.channels.items[self.channels.items.len-1];
|
rl.unloadTexture(self.grab_texture.active);
|
||||||
// }
|
}
|
||||||
|
|
||||||
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
|
fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 {
|
||||||
try file.seekTo(0);
|
try file.seekTo(0);
|
||||||
@ -103,6 +136,202 @@ fn nanoToSeconds(ns: i128) f32 {
|
|||||||
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
|
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void {
|
||||||
|
const samples = try readSamplesFromFile(self.allocator, file);
|
||||||
|
errdefer self.allocator.free(samples);
|
||||||
|
|
||||||
|
var min_value = samples[0];
|
||||||
|
var max_value = samples[0];
|
||||||
|
|
||||||
|
for (samples) |sample| {
|
||||||
|
min_value = @min(min_value, sample);
|
||||||
|
max_value = @max(max_value, sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
const margin = 0.1;
|
||||||
|
try self.channels.append(Channel{
|
||||||
|
.min_value = min_value,
|
||||||
|
.max_value = max_value,
|
||||||
|
.view_rect = .{
|
||||||
|
.from = 0,
|
||||||
|
.to = @floatFromInt(samples.len),
|
||||||
|
.min_value = min_value + (min_value - max_value) * margin,
|
||||||
|
.max_value = max_value + (max_value - min_value) * margin
|
||||||
|
},
|
||||||
|
.samples = .{ .owned = samples }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn showChannelMinimap(self: *App, channel: *Channel, minimap_rect: rl.Rectangle) void {
|
||||||
|
const MarkerState = enum {
|
||||||
|
normal,
|
||||||
|
hot,
|
||||||
|
active
|
||||||
|
};
|
||||||
|
|
||||||
|
var from_state = MarkerState.normal;
|
||||||
|
var to_state = MarkerState.normal;
|
||||||
|
var area_state = MarkerState.normal;
|
||||||
|
|
||||||
|
const sample_count = channel.samples.owned.len;
|
||||||
|
const sample_count_f32: f32 = @floatFromInt(sample_count);
|
||||||
|
|
||||||
|
const grab_marker_width: f32 = @floatFromInt(self.grab_texture.normal.width);
|
||||||
|
const view_column_min = RectUtils.left(minimap_rect) + grab_marker_width/2;
|
||||||
|
const view_column_max = RectUtils.right(minimap_rect) - grab_marker_width/2;
|
||||||
|
|
||||||
|
const from_column_position = remap(f32,
|
||||||
|
0,
|
||||||
|
sample_count_f32,
|
||||||
|
view_column_min,
|
||||||
|
view_column_max,
|
||||||
|
channel.view_rect.from
|
||||||
|
);
|
||||||
|
|
||||||
|
const to_column_position = remap(f32,
|
||||||
|
0,
|
||||||
|
sample_count_f32,
|
||||||
|
view_column_min,
|
||||||
|
view_column_max,
|
||||||
|
channel.view_rect.to
|
||||||
|
);
|
||||||
|
|
||||||
|
const visible_area_rect = rl.Rectangle{
|
||||||
|
.x = from_column_position,
|
||||||
|
.y = minimap_rect.y,
|
||||||
|
.width = to_column_position - from_column_position,
|
||||||
|
.height = minimap_rect.height
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouse = self.ui.getMousePosition();
|
||||||
|
if (RectUtils.isInsideVec2(minimap_rect, mouse)) {
|
||||||
|
rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.gray);
|
||||||
|
|
||||||
|
const grab_distance = 20;
|
||||||
|
|
||||||
|
if (@abs(mouse.x - from_column_position) < grab_distance) {
|
||||||
|
from_state = .hot;
|
||||||
|
} else if (@abs(mouse.x - to_column_position) < grab_distance) {
|
||||||
|
to_state = .hot;
|
||||||
|
} else if (RectUtils.isInsideVec2(visible_area_rect, mouse)) {
|
||||||
|
area_state = .hot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||||
|
if (area_state == .hot) {
|
||||||
|
channel.dragged_marker = .both;
|
||||||
|
} else if (from_state == .hot) {
|
||||||
|
channel.dragged_marker = .from;
|
||||||
|
} else if (to_state == .hot) {
|
||||||
|
channel.dragged_marker = .to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.black);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rl.isMouseButtonReleased(.mouse_button_left)) {
|
||||||
|
channel.dragged_marker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sample_under_mouse = remap(f32,
|
||||||
|
view_column_min,
|
||||||
|
view_column_max,
|
||||||
|
0,
|
||||||
|
sample_count_f32,
|
||||||
|
mouse.x
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channel.dragged_marker) |marker| {
|
||||||
|
const min_shown_samples = 1000;
|
||||||
|
switch (marker) {
|
||||||
|
.from => {
|
||||||
|
from_state = .active;
|
||||||
|
channel.view_rect.from = std.math.clamp(sample_under_mouse, 0, channel.view_rect.to-min_shown_samples);
|
||||||
|
},
|
||||||
|
.to => {
|
||||||
|
to_state = .active;
|
||||||
|
channel.view_rect.to = std.math.clamp(sample_under_mouse, channel.view_rect.from+min_shown_samples, @as(f32, sample_count_f32));
|
||||||
|
},
|
||||||
|
.both => {
|
||||||
|
area_state = .active;
|
||||||
|
from_state = .active;
|
||||||
|
to_state = .active;
|
||||||
|
|
||||||
|
var delta = remap(f32,
|
||||||
|
0,
|
||||||
|
minimap_rect.width,
|
||||||
|
0,
|
||||||
|
sample_count_f32,
|
||||||
|
self.ui.getMouseDelta().x
|
||||||
|
);
|
||||||
|
delta = std.math.clamp(delta, -channel.view_rect.from, sample_count_f32 - channel.view_rect.to);
|
||||||
|
|
||||||
|
channel.view_rect.from += delta;
|
||||||
|
channel.view_rect.to += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.drawRectangleRec(visible_area_rect, rl.Color.gray);
|
||||||
|
|
||||||
|
{
|
||||||
|
const texture = switch (from_state) {
|
||||||
|
.normal => self.grab_texture.normal,
|
||||||
|
.hot => self.grab_texture.hot,
|
||||||
|
.active => self.grab_texture.active
|
||||||
|
};
|
||||||
|
|
||||||
|
rl.drawTextureV(
|
||||||
|
texture,
|
||||||
|
.{
|
||||||
|
.x = from_column_position - @as(f32, @floatFromInt(texture.width)) / 2,
|
||||||
|
.y = RectUtils.top(minimap_rect)
|
||||||
|
},
|
||||||
|
rl.Color.white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const texture = switch (to_state) {
|
||||||
|
.normal => self.grab_texture.normal,
|
||||||
|
.hot => self.grab_texture.hot,
|
||||||
|
.active => self.grab_texture.active
|
||||||
|
};
|
||||||
|
|
||||||
|
rl.drawTextureV(
|
||||||
|
texture,
|
||||||
|
.{
|
||||||
|
.x = to_column_position - @as(f32, @floatFromInt(texture.width)) / 2,
|
||||||
|
.y = RectUtils.top(minimap_rect)
|
||||||
|
},
|
||||||
|
rl.Color.white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph_height = 128;
|
||||||
|
const minimap_height = 16;
|
||||||
|
const channel_height = graph_height + minimap_height;
|
||||||
|
fn showChannelRow(self: *App, channel: *Channel, channel_rect: rl.Rectangle) void {
|
||||||
|
const graph_rect, const minimap_rect = RectUtils.horizontalSplit(channel_rect, graph_height);
|
||||||
|
|
||||||
|
{ // Graph
|
||||||
|
Graph.draw(
|
||||||
|
&self.ui,
|
||||||
|
&channel.view_cache,
|
||||||
|
graph_rect,
|
||||||
|
channel.view_rect,
|
||||||
|
channel.samples.owned
|
||||||
|
);
|
||||||
|
|
||||||
|
rl.drawRectangleLinesEx(graph_rect, 1, rl.Color.black);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showChannelMinimap(channel, minimap_rect);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tick(self: *App) !void {
|
pub fn tick(self: *App) !void {
|
||||||
// const dt = rl.getFrameTime();
|
// const dt = rl.getFrameTime();
|
||||||
|
|
||||||
@ -134,29 +363,7 @@ pub fn tick(self: *App) !void {
|
|||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
// TODO: Handle error
|
// TODO: Handle error
|
||||||
const samples = try readSamplesFromFile(self.allocator, file);
|
try self.appendChannelFromFile(file);
|
||||||
errdefer self.allocator.free(samples);
|
|
||||||
|
|
||||||
var min_value = samples[0];
|
|
||||||
var max_value = samples[0];
|
|
||||||
|
|
||||||
for (samples) |sample| {
|
|
||||||
min_value = @min(min_value, sample);
|
|
||||||
max_value = @max(max_value, sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
const margin = 0.1;
|
|
||||||
try self.channels.append(Channel{
|
|
||||||
.min_value = min_value,
|
|
||||||
.max_value = max_value,
|
|
||||||
.view_rect = .{
|
|
||||||
.from = 0,
|
|
||||||
.to = @floatFromInt(samples.len),
|
|
||||||
.min_value = min_value + (min_value - max_value) * margin,
|
|
||||||
.max_value = max_value + (max_value - min_value) * margin
|
|
||||||
},
|
|
||||||
.samples = .{ .owned = samples }
|
|
||||||
});
|
|
||||||
} else |err| {
|
} else |err| {
|
||||||
// TODO: Show error message to user;
|
// TODO: Show error message to user;
|
||||||
log.err("Failed to pick file: {}", .{ err });
|
log.err("Failed to pick file: {}", .{ err });
|
||||||
@ -165,22 +372,11 @@ pub fn tick(self: *App) !void {
|
|||||||
|
|
||||||
{
|
{
|
||||||
var channels_stack = UI.Stack.init(RectUtils.shrink(graphs_rect, 10), .top_to_bottom);
|
var channels_stack = UI.Stack.init(RectUtils.shrink(graphs_rect, 10), .top_to_bottom);
|
||||||
|
|
||||||
for (self.channels.items) |*channel| {
|
for (self.channels.items) |*channel| {
|
||||||
const channel_rect = channels_stack.next(128);
|
self.showChannelRow(channel, channels_stack.next(channel_height));
|
||||||
|
|
||||||
Graph.draw(
|
|
||||||
&channel.view_cache,
|
|
||||||
channel_rect,
|
|
||||||
channel.view_rect,
|
|
||||||
channel.samples.owned
|
|
||||||
);
|
|
||||||
|
|
||||||
rl.drawRectangleLinesEx(channel_rect, 1, rl.Color.black);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// rl.drawLineV(
|
// rl.drawLineV(
|
||||||
// Vec2.init(0, window_height/2),
|
// Vec2.init(0, window_height/2),
|
||||||
// Vec2.init(window_width, window_height/2),
|
// Vec2.init(window_width, window_height/2),
|
||||||
|
66
src/aseprite.zig
Normal file
66
src/aseprite.zig
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const rl = @import("raylib");
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("cute_aseprite.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
ase: *c.ase_t,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, memory: []const u8) !@This() {
|
||||||
|
_ = allocator; // TODO: Pass allocator to function
|
||||||
|
|
||||||
|
const parsed = c.cute_aseprite_load_from_memory(@ptrCast(memory), @intCast(memory.len), null);
|
||||||
|
if (parsed == null) {
|
||||||
|
return error.CuteLoadFromMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @This(){
|
||||||
|
.ase = @ptrCast(parsed)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: @This()) void {
|
||||||
|
c.cute_aseprite_free(self.ase);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTag(self: @This(), name: []const u8) ?c.struct_ase_tag_t {
|
||||||
|
const tag_count: usize = @intCast(self.ase.tag_count);
|
||||||
|
for (self.ase.tags[0..tag_count]) |tag| {
|
||||||
|
const tag_name = std.mem.span(tag.name);
|
||||||
|
if (std.mem.eql(u8, tag_name, name)) {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getFrameImage(self: @This(), frame_index: usize) rl.Image {
|
||||||
|
const width: usize = @intCast(self.ase.w);
|
||||||
|
const height: usize = @intCast(self.ase.h);
|
||||||
|
|
||||||
|
var image = rl.genImageColor(@intCast(width), @intCast(height), rl.Color.black.alpha(0));
|
||||||
|
assert(@intFromPtr(image.data) != 0);
|
||||||
|
|
||||||
|
image.setFormat(rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8);
|
||||||
|
|
||||||
|
const pixel_count = width * height;
|
||||||
|
const frame = self.ase.frames[frame_index];
|
||||||
|
for (0.., frame.pixels[0..pixel_count]) |pixel_index, pixel| {
|
||||||
|
const x = @mod(pixel_index, width);
|
||||||
|
const y = @divFloor(pixel_index, height);
|
||||||
|
image.drawPixel(@intCast(x), @intCast(y), .{
|
||||||
|
.r = pixel.r,
|
||||||
|
.g = pixel.g,
|
||||||
|
.b = pixel.b,
|
||||||
|
.a = pixel.a,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTagImage(self: @This(), tag: c.struct_ase_tag_t) rl.Image {
|
||||||
|
return getFrameImage(self, @intCast(tag.from_frame));
|
||||||
|
}
|
BIN
src/assets/grab-marker.ase
Normal file
BIN
src/assets/grab-marker.ase
Normal file
Binary file not shown.
BIN
src/assets/srcery-pallete.ase
Normal file
BIN
src/assets/srcery-pallete.ase
Normal file
Binary file not shown.
10
src/cute_aseprite.c
Normal file
10
src/cute_aseprite.c
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// TODO: Use zig allocators
|
||||||
|
// extern void *zig_cute_aseprite_malloc(void* ctx, size_t size);
|
||||||
|
// extern void zig_cute_aseprite_free(void* ctx, void* mem);
|
||||||
|
|
||||||
|
#define CUTE_ASEPRITE_IMPLEMENTATION
|
||||||
|
// #define CUTE_ASEPRITE_ALLOC(size, ctx) zig_cute_aseprite_malloc(ctx, size)
|
||||||
|
// #define CUTE_ASEPRITE_FREE(mem, ctx) zig_cute_aseprite_free(ctx, mem)
|
||||||
|
#include "cute_aseprite.h"
|
1358
src/cute_aseprite.h
Normal file
1358
src/cute_aseprite.h
Normal file
File diff suppressed because it is too large
Load Diff
15
src/main.zig
15
src/main.zig
@ -174,6 +174,21 @@ pub fn main() !void {
|
|||||||
);
|
);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
||||||
|
if (builtin.mode == .Debug) {
|
||||||
|
{
|
||||||
|
const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin", .{});
|
||||||
|
defer sample_file.close();
|
||||||
|
try app.appendChannelFromFile(sample_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin", .{});
|
||||||
|
defer sample_file.close();
|
||||||
|
try app.appendChannelFromFile(sample_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
while (!rl.windowShouldClose()) {
|
while (!rl.windowShouldClose()) {
|
||||||
try app.tick();
|
try app.tick();
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
|
const builtin = @import("builtin");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const rl = @import("raylib");
|
const rl = @import("raylib");
|
||||||
const Theme = @import("./theme.zig");
|
const Theme = @import("../theme.zig");
|
||||||
|
const UI = @import("./root.zig");
|
||||||
|
|
||||||
|
const remap = @import("../utils.zig").remap;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Vec2 = rl.Vector2;
|
const Vec2 = rl.Vector2;
|
||||||
const clamp = std.math.clamp;
|
const clamp = std.math.clamp;
|
||||||
|
|
||||||
|
const disable_caching = false;
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
if (builtin.mode != .Debug) {
|
||||||
|
assert(disable_caching == false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const ViewOptions = struct {
|
pub const ViewOptions = struct {
|
||||||
from: f32, // inclusive
|
from: f32, // inclusive
|
||||||
to: f32, // exclusive
|
to: f32, // inclusive
|
||||||
min_value: f64,
|
min_value: f64,
|
||||||
max_value: f64,
|
max_value: f64,
|
||||||
left_aligned: bool = true,
|
left_aligned: bool = true,
|
||||||
@ -31,12 +42,25 @@ pub const Cache = struct {
|
|||||||
pub fn invalidate(self: *Cache) void {
|
pub fn invalidate(self: *Cache) void {
|
||||||
self.options = null;
|
self.options = null;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T {
|
pub fn draw(self: Cache, rect: rl.Rectangle) void {
|
||||||
const t = (value - from_min) / (from_max - from_min);
|
if (self.texture) |texture| {
|
||||||
return std.math.lerp(to_min, to_max, t);
|
const source = rl.Rectangle{
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.width = @floatFromInt(texture.texture.width),
|
||||||
|
.height = @floatFromInt(-texture.texture.height)
|
||||||
|
};
|
||||||
|
rl.drawTexturePro(
|
||||||
|
texture.texture,
|
||||||
|
source,
|
||||||
|
rect,
|
||||||
|
rl.Vector2.zero(),
|
||||||
|
0, rl.Color.white
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 {
|
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 {
|
||||||
return remap(
|
return remap(
|
||||||
@ -73,9 +97,9 @@ fn clampIndexUsize(value: f32, size: usize) usize {
|
|||||||
return @intFromFloat(clamp(value, 0, size_f32));
|
return @intFromFloat(clamp(value, 0, size_f32));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||||
assert(options.left_aligned); // TODO:
|
assert(options.left_aligned); // TODO:
|
||||||
assert(options.to > options.from);
|
assert(options.to >= options.from);
|
||||||
|
|
||||||
if (options.from > @as(f32, @floatFromInt(samples.len))) return;
|
if (options.from > @as(f32, @floatFromInt(samples.len))) return;
|
||||||
if (options.to < 0) return;
|
if (options.to < 0) return;
|
||||||
@ -130,13 +154,8 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rl.beginScissorMode(
|
ui.beginScissorModeRect(draw_rect);
|
||||||
@intFromFloat(@round(draw_rect.x)),
|
defer ui.endScissorMode();
|
||||||
@intFromFloat(@round(draw_rect.y)),
|
|
||||||
@intFromFloat(@round(draw_rect.width)),
|
|
||||||
@intFromFloat(@round(draw_rect.height))
|
|
||||||
);
|
|
||||||
defer rl.endScissorMode();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
||||||
@ -166,7 +185,12 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
pub fn draw(ui: *UI, cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||||
|
if (disable_caching) {
|
||||||
|
drawSamples(ui, draw_rect, options, samples);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cache) |c| {
|
if (cache) |c| {
|
||||||
const render_width: i32 = @intFromFloat(@ceil(draw_rect.width));
|
const render_width: i32 = @intFromFloat(@ceil(draw_rect.width));
|
||||||
const render_height: i32 = @intFromFloat(@ceil(draw_rect.height));
|
const render_height: i32 = @intFromFloat(@ceil(draw_rect.height));
|
||||||
@ -191,35 +215,26 @@ pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, sampl
|
|||||||
const render_texture = c.texture.?;
|
const render_texture = c.texture.?;
|
||||||
|
|
||||||
if (c.options != null and std.meta.eql(c.options.?, options)) {
|
if (c.options != null and std.meta.eql(c.options.?, options)) {
|
||||||
const source = rl.Rectangle{
|
c.draw(draw_rect);
|
||||||
.x = 0,
|
|
||||||
.y = 0,
|
|
||||||
.width = @floatFromInt(render_texture.texture.width),
|
|
||||||
.height = @floatFromInt(-render_texture.texture.height)
|
|
||||||
};
|
|
||||||
rl.drawTexturePro(
|
|
||||||
render_texture.texture,
|
|
||||||
source,
|
|
||||||
draw_rect,
|
|
||||||
rl.Vector2.zero(),
|
|
||||||
0, rl.Color.white
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
c.options = options;
|
c.options = options;
|
||||||
render_texture.begin();
|
render_texture.begin();
|
||||||
|
|
||||||
rl.gl.rlPushMatrix();
|
ui.pushTransform();
|
||||||
rl.gl.rlTranslatef(-draw_rect.x, -draw_rect.y, 0);
|
ui.transformTranslate(-draw_rect.x, -draw_rect.y);
|
||||||
|
rl.clearBackground(rl.Color.black.alpha(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSamples(draw_rect, options, samples);
|
drawSamples(ui, draw_rect, options, samples);
|
||||||
|
|
||||||
if (cache) |c| {
|
if (cache) |c| {
|
||||||
rl.gl.rlPopMatrix();
|
ui.popTransform();
|
||||||
|
|
||||||
const render_texture = c.texture.?;
|
const render_texture = c.texture.?;
|
||||||
render_texture.end();
|
render_texture.end();
|
||||||
|
|
||||||
|
c.draw(draw_rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,3 +24,8 @@ pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
|
|||||||
.height = size
|
.height = size
|
||||||
}, color);
|
}, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub 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);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user