daq-view/src/graph.zig

242 lines
7.3 KiB
Zig

const builtin = @import("builtin");
const std = @import("std");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const RangeF64 = @import("./range.zig").RangeF64;
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 {
x_range: RangeF64,
y_range: RangeF64,
color: rl.Color = srcery.red,
};
pub const Cache = struct {
const Key = struct {
options: ViewOptions,
drawn_x_range: RangeF64
};
texture: ?rl.RenderTexture2D = null,
key: ?Key = null,
pub fn deinit(self: *Cache) void {
if (self.texture) |texture| {
texture.unload();
self.texture = null;
}
self.key = null;
}
pub fn invalidate(self: *Cache) void {
self.key = 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 drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
rl.beginScissorMode(
@intFromFloat(draw_rect.x),
@intFromFloat(draw_rect.y),
@intFromFloat(draw_rect.width),
@intFromFloat(draw_rect.height),
);
defer rl.endScissorMode();
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
if (i_range.lower > i_range.upper) {
return;
}
const from_i: usize = @intFromFloat(i_range.lower);
const to_i: usize = @intFromFloat(i_range.upper);
if (to_i == 0 or from_i == to_i) {
return;
}
for (from_i..(to_i-1)) |i| {
const i_f64: f64 = @floatFromInt(i);
rl.drawLineV(
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i])),
},
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i + 1])),
},
options.color
);
}
}
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
if (i_range.lower > i_range.upper) {
return;
}
const samples_per_column = options.x_range.size() / draw_x_range.size();
assert(samples_per_column >= 1);
var i = i_range.lower;
while (i < i_range.upper - samples_per_column) : (i += samples_per_column) {
const column_start: usize = @intFromFloat(i);
const column_end: usize = @intFromFloat(i + samples_per_column);
const column_samples = samples[column_start..column_end];
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.x_range.remapTo(draw_x_range, i);
const y_min = options.y_range.remapTo(draw_y_range, column_min);
const y_max = options.y_range.remapTo(draw_y_range, 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
);
}
}
}
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
const x_range = options.x_range;
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
if (x_range.upper < 0) return;
const samples_per_column = x_range.size() / draw_rect.width;
if (samples_per_column >= 2) {
drawSamplesApproximate(draw_rect, options, samples);
} else {
drawSamplesExact(draw_rect, options, samples);
}
}
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.key = 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.?;
const cache_key = Cache.Key{
.options = options,
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)).intersectPositive(options.x_range)
};
if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) {
// Cached graph hasn't changed, no need to redraw.
return;
}
cache.key = cache_key;
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);
}
}