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); } }