260 lines
8.1 KiB
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);
|
|
}
|
|
} |