daq-view/src/graph.zig

260 lines
8.1 KiB
Zig

const builtin = @import("builtin");
const std = @import("std");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const remap = @import("./utils.zig").remap;
const assert = std.debug.assert;
const Vec2 = rl.Vector2;
const Rect = rl.Rectangle;
const clamp = std.math.clamp;
const disable_caching = false;
comptime {
// Just making sure that release build has caching enabled
if (builtin.mode != .Debug) {
assert(disable_caching == false);
}
}
pub const ViewOptions = struct {
from: f32, // inclusive
to: f32, // inclusive
min_value: f64,
max_value: f64,
left_aligned: bool = true,
color: rl.Color = srcery.red,
pub fn mapSampleIndexToX(self: ViewOptions, to_x: f64, to_width: f64, index: f64) f64 {
return remap(
f64,
self.from, self.to,
to_x, to_x + to_width,
index
);
}
pub fn mapSampleXToIndex(self: ViewOptions, from_x: f64, from_width: f64, x: f64) f64 {
return remap(
f64,
from_x, from_x + from_width,
self.from, self.to,
x
);
}
pub fn mapSampleValueToY(self: ViewOptions, to_y: f64, to_height: f64, sample: f64) f64 {
return remap(
f64,
self.min_value, self.max_value,
to_y + to_height, to_y,
sample
);
}
pub fn mapSampleYToValue(self: ViewOptions, to_y: f64, to_height: f64, y: f64) f64 {
return remap(
f64,
to_y + to_height, to_y,
self.min_value, self.max_value,
y
);
}
pub fn mapSampleVec2(self: ViewOptions, draw_rect: rl.Rectangle, index: f64, sample: f64) Vec2 {
return .{
.x = @floatCast(self.mapSampleIndexToX(draw_rect.x, draw_rect.width, index)),
.y = @floatCast(self.mapSampleValueToY(draw_rect.y, draw_rect.height, sample))
};
}
};
pub const ViewOptionsWithRect = struct {
view: ViewOptions,
rect: Rect,
pub fn mapSampleIndexToX(self: ViewOptionsWithRect, index: f64) f64 {
return self.view.mapSampleIndexToX(self.rect.x, self.rect.width, index);
}
};
pub const Cache = struct {
texture: ?rl.RenderTexture2D = null,
options: ?ViewOptions = null,
pub fn deinit(self: *Cache) void {
if (self.texture) |texture| {
texture.unload();
self.texture = null;
}
self.options = null;
}
pub fn invalidate(self: *Cache) void {
self.options = null;
}
pub fn draw(self: Cache, rect: rl.Rectangle) void {
if (self.texture) |texture| {
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 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 drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
assert(options.left_aligned); // TODO:
assert(options.to >= options.from);
if (options.from > @as(f32, @floatFromInt(samples.len))) return;
if (options.to < 0) return;
const sample_count = options.to - options.from;
const samples_per_column = sample_count / draw_rect.width;
const samples_threshold = 2;
if (samples_per_column >= samples_threshold) {
var i = clampIndex(options.from, samples.len);
while (i < clampIndex(options.to, samples.len)) : (i += samples_per_column) {
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 = options.mapSampleIndexToX(draw_rect.x, draw_rect.width, @floatFromInt(from_index));
const y_min = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_min);
const y_max = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_max);
if (@abs(y_max - y_min) < 1) {
const avg = (y_min + y_max) / 2;
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(avg) },
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
options.color
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
options.color
);
}
}
} else {
rl.beginScissorMode(
@intFromFloat(draw_rect.x),
@intFromFloat(draw_rect.y),
@intFromFloat(draw_rect.width),
@intFromFloat(draw_rect.height),
);
defer rl.endScissorMode();
const from_index = clampIndexUsize(@floor(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
if (to_index - from_index > 0) {
for (from_index..(to_index-1)) |i| {
const from_point = options.mapSampleVec2(draw_rect, @floatFromInt(i), samples[i]);
const to_point = options.mapSampleVec2(draw_rect, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, options.color);
}
}
}
}
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
if (render_width <= 0 or render_height <= 0) {
return;
}
// Unload render texture if rendering width or height changed
if (cache.texture) |render_texture| {
const texure = render_texture.texture;
if (texure.width != render_width or texure.height != render_height) {
render_texture.unload();
cache.texture = null;
cache.options = null;
}
}
if (cache.texture == null) {
const texture = rl.loadRenderTexture(render_width, render_height);
// TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory?
assert(rl.isRenderTextureReady(texture));
cache.texture = texture;
}
const render_texture = cache.texture.?;
if (cache.options != null and std.meta.eql(cache.options.?, options)) {
// Cached graph hasn't changed, no need to redraw.
return;
}
cache.options = options;
render_texture.begin();
defer render_texture.end();
rl.gl.rlPushMatrix();
defer rl.gl.rlPopMatrix();
rl.clearBackground(rl.Color.black.alpha(0));
rl.gl.rlTranslatef(0, render_size.y, 0);
rl.gl.rlScalef(1, -1, 1);
const draw_rect = rl.Rectangle{
.x = 0,
.y = 0,
.width = render_size.x,
.height = render_size.y
};
drawSamples(draw_rect, options, samples);
}
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
if (draw_rect.width < 0 or draw_rect.height < 0) {
return;
}
if (cache != null and !disable_caching) {
const c = cache.?;
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
c.draw(draw_rect);
} else {
drawSamples(draw_rect, options, samples);
}
}