PlotSettings :: struct { colors : PlotColors; fixed_bounds : bool; top_left : Vector2; bottom_right : Vector2; } PLOT_STYLE :: enum s32 { TICK_MARKS; SCALE_MARKER; } PLOT_DATA_STYLE :: enum { ONCE; EVERY_FRAME; } draw_plot :: (key: string, size: Vector2, xdata: []float64, yarrays: [][]float64, using settings: PlotSettings, data_style := PLOT_DATA_STYLE.ONCE) { plot, success := table_find(*plots, key); if !success { plot = New(Plot); init_plot(plot, key, xx size.x, xx size.y); table_set(*plots, key, plot); if data_style == .ONCE { plot.xfloat = NewArray(xdata.count, float); plot.xmin = 0.0; plot.xmax = 0.0; x_acc : float; for xdata { plot.xfloat[it_index] = cast(float) it; plot.xfloat[it_index] = cast(float) it; if it > plot.xmax plot.xmax = xx it; if it < plot.xmin plot.xmin = xx it; is_nan, is_inf := is_nan_is_inf(cast(float) it); if !is_nan && !is_inf { x_acc += cast(float) it; } } plot.x_avg = x_acc / xdata.count; plot.ymin = 0.0; plot.ymax = 0.0; y_acc : float; for ydata: yarrays { yfloat := NewArray(ydata.count, float); array_add(*plot.yfloats, yfloat); for ydata { yfloat[it_index] = cast(float) it; if it > plot.ymax plot.ymax = xx it; if it < plot.ymin plot.ymin = xx it; is_nan, is_inf := is_nan_is_inf(cast(float) it); if !is_nan && !is_inf { y_acc += cast(float) it; } } } plot.y_avg = y_acc / (yarrays[0].count * yarrays.count); if fixed_bounds { plot.xmin = top_left.x; plot.xmax = bottom_right.x; plot.ymin = bottom_right.y; plot.ymax = top_left.y; plot.x_avg = plot.xmax - plot.xmin; plot.y_avg = plot.ymax - plot.ymin; } } plot.zoom = 1.0; } if data_style == .EVERY_FRAME { plot.xfloat = NewArray(xdata.count, float); plot.xmin = FLOAT32_MAX; plot.xmax = -FLOAT32_MAX; x_acc : float; for xdata { plot.xfloat[it_index] = cast(float) it; if it > plot.xmax plot.xmax = xx it; if it < plot.xmin plot.xmin = xx it; is_nan, is_inf := is_nan_is_inf(cast(float) it); if !is_nan && !is_inf { x_acc += cast(float) it; } } plot.x_avg = x_acc / xdata.count; plot.ymin = 0.0; plot.ymax = 0.0; y_acc : float; for ydata: yarrays { yfloat := NewArray(ydata.count, float); array_add(*plot.yfloats, yfloat); for ydata { yfloat[it_index] = cast(float) it; if it > plot.ymax plot.ymax = xx it; if it < plot.ymin plot.ymin = xx it; is_nan, is_inf := is_nan_is_inf(cast(float) it); if !is_nan && !is_inf { y_acc += cast(float) it; } } } plot.y_avg = y_acc / (yarrays[0].count * yarrays.count); if fixed_bounds { plot.xmin = top_left.x; plot.xmax = bottom_right.x; plot.ymin = bottom_right.y; plot.ymax = top_left.y; plot.x_avg = plot.xmax - plot.xmin; plot.y_avg = plot.ymax - plot.ymin; } } ImGui.RadioButton("Tick marks", xx *plot.plot_style, 0); ImGui.SameLine(); ImGui.RadioButton("Scale marker", xx *plot.plot_style, 1); child_size := ImGui.GetContentRegionAvail(); child_size.y = 260; child_pos := ImGui.GetCursorScreenPos(); if ImGui.BeginChild(key.data, child_size, ImGui.ChildFlags.None, ImGui.WindowFlags.HorizontalScrollbar) { if plot.plot_style == { case .TICK_MARKS; plot.left_bearing = 40; plot.bottom_bearing = 20; case .SCALE_MARKER; plot.left_bearing = 0; plot.bottom_bearing = 20; } child_size = ImGui.GetContentRegionAvail(); child_pos = ImGui.GetCursorScreenPos(); plot.x = xx child_pos.x; plot.y = xx child_pos.y; if plot.width < 0 || plot.height < 0 { log("[Warning] The plot has dimensions lower than 0."); } else { if plot.width != cast(s32) size.x || plot.height != cast(s32) size.y { plot.width = xx size.x; plot.height = xx size.y; Simp.texture_resize_render_target(plot.texture, xx size.x, xx size.y); } ar : float = cast(float) plot.width / cast(float) plot.height; pixel_size_x : float = (plot.xmax - plot.xmin) / plot.width; pixel_size_y : float = (plot.ymax - plot.ymin) / plot.height; if is_in_rect(xy(xx plot.x, xx plot.y), xy(xx plot.width, xx plot.height), mouse) { if mouse_left { plot.pos.x += mouse_delta.x * pixel_size_x / plot.zoom; plot.pos.y += -mouse_delta.y * pixel_size_y / plot.zoom; } if mouse_wheel_zoom && !fixed_bounds { plot.zoom += plot.zoom * (cast(float) mouse_wheel_delta * 0.1); if plot.zoom < 0.01 { plot.zoom = 0.01; } } } reset_fbo : GLint; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, *reset_fbo); defer glBindFramebuffer(GL_FRAMEBUFFER, xx reset_fbo); reset_program : GLint; glGetIntegerv(GL_CURRENT_PROGRAM, *reset_program); defer glUseProgram(xx reset_program); Simp.set_render_target(plot.texture); Simp.clear_render_target(colors.background.x, colors.background.y, colors.background.z, 1.0); draw_axis(plot, plot.plot_style, colors); if plot.plot_style == .SCALE_MARKER draw_scale(plot, colors); glUseProgram(data_shader); glViewport(xx plot.left_bearing, xx plot.bottom_bearing, xx (plot.width - plot.left_bearing), xx (plot.height - plot.bottom_bearing)); glBindVertexArray(plot.vao); glBindBuffer(GL_ARRAY_BUFFER, plot.xvbo); glEnableVertexAttribArray(0); glBufferData(GL_ARRAY_BUFFER, plot.xfloat.count * size_of(float), plot.xfloat.data, GL_DYNAMIC_DRAW); glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, size_of(float), null); for yfloat : plot.yfloats { glBindBuffer(GL_ARRAY_BUFFER, plot.yvbo); glEnableVertexAttribArray(1); glBufferData(GL_ARRAY_BUFFER, yfloat.count * size_of(float), yfloat.data, GL_DYNAMIC_DRAW); glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, size_of(float), null); model := identity_of(Matrix4); model = translate(model, .{plot.pos.x, plot.pos.y, 0.0}); model = translate(model, .{plot.x_avg, plot.y_avg, 0.0}); model = translate(model, .{-plot.pos.x, -plot.pos.y, 0.0}); model = scale(model, .{plot.zoom, plot.zoom, 1.0}); model = translate(model, .{plot.pos.x, plot.pos.y, 0.0}); model = translate(model, .{-plot.x_avg, -plot.y_avg, 0.0}); proj := orthographic_projection_matrix((plot.xmin), (plot.xmax), (plot.ymin), (plot.ymax), -1.0, 1.0); glUniformMatrix4fv(glGetUniformLocation(data_shader, "model"), 1, GL_TRUE, xx *model); glUniformMatrix4fv(glGetUniformLocation(data_shader, "proj"), 1, GL_TRUE, xx *proj); color := data_colors[it_index % data_colors.count]; glUniform4f(glGetUniformLocation(data_shader, "data_color"), color.x, color.y, color.z, color.w); glDrawArrays(GL_LINE_STRIP, 0, xx (plot.xfloat.count)); primitives_rendered_this_frame += plot.xfloat.count - 1; } glBindFramebuffer(GL_FRAMEBUFFER, 0); ImGui.Image(cast(ImGui.ImTextureID) plot.texture.gl_handle, size, .{0.0, 1.0}, .{1.0, 0.0}); plot.last_frame = frame; } } ImGui.EndChild(); if data_style == .EVERY_FRAME { array_reset(*plot.xfloat); for * plot.yfloats { array_reset(it); remove it; } } } free_old_plots :: () { for plot: plots { if plot.last_frame < frame { free_plot(plot); table_remove(*plots, plot.key); } } } init :: (_window: Window_Type, _win_width: s32, _win_height: s32, _mouse_wheel_zoom : bool, font_directory: string, font_name: string) { window = _window; win_width = _win_width; win_height = _win_height; mouse_wheel_zoom = _mouse_wheel_zoom; JetBrainsMonoRegular = Simp.get_font_at_size(font_directory, font_name, 14); assert(JetBrainsMonoRegular != null); data_shader = make_shader(data_vertex_shader_text, data_fragment_shader_text); line_shader = make_shader(line_vertex_shader_text, line_fragment_shader_text); glEnable(GL_MULTISAMPLE); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glEnable(GL_LINE_SMOOTH); glLineWidth(1.5); } window_resize :: (_win_width: s32, _win_height: s32) { win_width = _win_width; win_height = _win_height; } new_frame :: () { frame += 1; primitives_rendered_this_frame = 0; mouse_delta = .{}; mouse_wheel_delta = 0; x, y := get_mouse_pointer_position(window, false); mouse = .{cast(float, x), cast(float, y)}; } handle_input :: (events: []Input.Event) { // @Hack Input module for android treats touch inputs as mouse button left, but you need to set the "mouse" // position to something valid before triggering the click so update it here. // @TODO(Charles): This can now be updated as modules/Input has touch! :ImGuiAndroidInput #if OS == .ANDROID { WC :: #import "Window_Creation"; Math :: #import "Math"; touch_x, touch_y, success := WC.get_mouse_pointer_position(true); touch_x_f := cast(float) touch_x; touch_y_f := cast(float) touch_y; if !success { // ImGui wants -FLT_MAX, -FLT_MAX to indicate no mouse. FLT_MAX :: Math.FLOAT32_MAX; touch_x_f = -FLT_MAX; touch_y_f = -FLT_MAX; } // @Hack when releasing screen, need to have the mouse position set to where we released. touch_on_release := mouse; mouse = .{touch_x_f, touch_y_f}; } // NOTE(Charles): On windows mouse position is handled in ImGui_ImplWin32_NewFrame with GetCusorPos on tick. The // Imgui examples also respond to WM_MOUSEMOVE, but it's not clear why? // modules/Input "doesn't have mouse move events yet", it only gives us the delta. for event: events { // :Win32InputMissing AddFocusEvent, could be done by checking Input.input_application_has_focus for change. if event.type == { case .KEYBOARD; if event.key_code == { // :Win32InputMissing mouse extra buttons, SetCapturing?, Mouse source // case .MOUSE_BUTTON_LEFT; io.AddMouseButtonEvent(io, xx ImGui.MouseButton.Left , xx event.key_pressed); case .MOUSE_BUTTON_LEFT; // @Hack #if OS == .ANDROID { if !event.key_pressed { mouse = .{touch_on_release.x, touch_on_release.y}; } } if event.key_pressed mouse_left = true; else mouse_left = false; case .MOUSE_BUTTON_RIGHT; //io.AddMouseButtonEvent(io, xx ImGui.MouseButton.Right , xx event.key_pressed); case .MOUSE_BUTTON_MIDDLE; //io.AddMouseButtonEvent(io, xx ImGui.MouseButton.Middle, xx event.key_pressed); case; //imgui_key := to_imgui_key(event.key_code); //// :Win32InputMissing Input only gives us single ctrl, shift, alt, not left/right. ImGui example sends //// one event for either, then an extra for left right. //if imgui_key != .None { // // :Win32InputMissing SetKeyEventNativeData gets called for some legacy reason? // io.AddKeyEvent(io, imgui_key, xx event.key_pressed); //} } case .MOUSE_WHEEL; mouse_wheel_delta += event.wheel_delta / cast(float, event.typical_wheel_delta); } } mouse_delta += .{cast(float, Input.mouse_delta_x), cast(float, Input.mouse_delta_y)}; } get_primitives_rendered :: () -> int { return primitives_rendered_this_frame; } JetBrainsMonoRegular : *Simp.Dynamic_Font; #scope_file #import "Basic"; #import "GL"; #import "Hash_Table"; #import "Math"; ImGui :: #import,dir "../imgui-lib"; Simp :: #import "Simp"; #import "stb_rect_pack"; #import "File"; Input :: #import "Input"; #import "Window_Creation"; window : Window_Type; frame : s64; mouse : Vector2; mouse_delta : Vector2; mouse_wheel_delta : float; mouse_left : bool; primitives_rendered_this_frame : int; win_width : s32; win_height : s32; mouse_wheel_zoom : bool; PlotColors :: struct { background : Vector3; lines : Vector3; text : Vector3; } Plot :: struct { key : string; plot_style : PLOT_STYLE; texture: *Simp.Texture; vao : GLuint; xvbo : GLuint; yvbo : GLuint; xfloat : []float; yfloats : [..][]float; x : s32; y : s32; width : s32; height : s32; xmin : float; xmax : float; ymax : float; ymin : float; x_avg : float; y_avg : float; left_bearing : float; bottom_bearing : float; pos : Vector2; zoom : float; last_frame : s64; } plots : Table(string, *Plot); data_colors := Vector4.[ .{1.0, 0.0, 0.0, 1.0}, .{0.0, 1.0, 0.0, 1.0}, .{0.0, 0.0, 1.0, 1.0}, .{0.76, 0.56, 0.1, 1.0}, .{0.8, 0.2, 0.2, 1.0}, .{0.1, 0.56, 0.48, 1.0}, ]; free_plot :: (plot: *Plot) { glDeleteVertexArrays(1, *plot.vao); glDeleteBuffers(1, *plot.xvbo); glDeleteBuffers(1, *plot.yvbo); array_free(plot.xfloat); for plot.yfloats { array_free(it); } } init_plot :: (plot: *Plot, key: string, width: s32, height: s32) { plot.key = key; plot.width = 1024; plot.height = 1024; plot.texture = Simp.texture_create_render_target(width, height, .RGBA8, .sRGB); glGenVertexArrays(1, *plot.vao); glBindVertexArray(plot.vao); glGenBuffers(1, *plot.xvbo); glGenBuffers(1, *plot.yvbo); } truncate :: (x: float, n: s32) -> float { ret : float = x; ret *= pow(10.0, xx n); ret = floor(ret); ret /= pow(10.0, xx n); return ret; } draw_axis :: (using plot: Plot, style: PLOT_STYLE, colors: PlotColors) { ar : float = cast(float) width / cast(float) height; content_x : s32 = x + xx left_bearing; content_y : s32; content_width : s32 = width - xx left_bearing; content_height : s32 = height - xx bottom_bearing; extent_x := xmax - xmin; extent_y := ymax - ymin; positions : [..]Vector2; defer array_reset(*positions); array_add(*positions, .{0.0, 0.0}); array_add(*positions, .{xx (content_width ), 0.0}); array_add(*positions, .{xx (content_width ), 0.0}); array_add(*positions, .{xx (content_width ), xx (content_height )}); array_add(*positions, .{xx (content_width ), xx (content_height )}); array_add(*positions, .{0.0, xx (content_height )}); array_add(*positions, .{0.0, xx (content_height )}); array_add(*positions, .{0.0, 0.0}); if plot.xfloat.count > 0 { x_ticks := 10; for 0..x_ticks { xpos : s64 = cast(s64) (cast(float) it / cast(float) x_ticks * cast(float) (content_width)); xxx := (((cast(float) it / cast(float) x_ticks) - 0.5) / zoom) + 0.5; xxx = xxx * (xmax - (pos.x)) + (1 - xxx) * (xmin - (pos.x)); array_add(*positions, .{cast(float) xpos, 0.0}); array_add(*positions, .{cast(float) xpos, cast(float) (content_height )}); label_width := Simp.prepare_text(JetBrainsMonoRegular, tprint("%", formatFloat(cast(float) xxx, trailing_width = 1, zero_removal = .NO))); if style == .TICK_MARKS { Simp.draw_prepared_text(JetBrainsMonoRegular, xx (xpos + left_bearing - (label_width / 2.0)), xx (4.0), .{1, 1, 1, 1}); } } y_ticks := 10; for 0..y_ticks { ypos : s64 = cast(s64) (cast(float) it / cast(float) y_ticks * (content_height )); yyy := ((0.5) - cast(float) it / cast(float) y_ticks) / zoom + 0.5; yyy = yyy * (ymin - pos.y) + (1 - yyy) * (ymax - pos.y); array_add(*positions, .{0.0, cast(float) ypos}); array_add(*positions, .{cast(float) (content_width ), cast(float) ypos}); label_width := Simp.prepare_text(JetBrainsMonoRegular, tprint("%", formatFloat(cast(float) yyy, trailing_width = 1, zero_removal = .NO))); if style == .TICK_MARKS { Simp.draw_prepared_text(JetBrainsMonoRegular, xx (xx cast(s64) (left_bearing - label_width - 4.0)), xx (cast(s64) (ypos + JetBrainsMonoRegular.character_height)), .{1, 1, 1, 1}); } } } glViewport(xx left_bearing, xx bottom_bearing, xx (width - left_bearing), xx (height - bottom_bearing)); axis_vao : GLuint; glGenVertexArrays(1, *axis_vao); glBindVertexArray(axis_vao); axis_vbo : GLuint; glGenBuffers(1, *axis_vbo); glBindBuffer(GL_ARRAY_BUFFER, axis_vbo); glBufferData(GL_ARRAY_BUFFER, positions.count * size_of(Vector2), positions.data, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, size_of(Vector2), null); proj := orthographic_projection_matrix(0.0, cast(float) (content_width ), 0.0, cast(float) (content_height ), -1.0, 1.0); glUseProgram(line_shader); glUniformMatrix4fv(glGetUniformLocation(line_shader, "proj"), 1, GL_TRUE, xx *proj); glUniform4f(glGetUniformLocation(line_shader, "data_color"), colors.lines.x, colors.lines.y, colors.lines.z, 1.0); glDrawArrays(GL_LINES, 0, xx positions.count); primitives_rendered_this_frame += positions.count / 2; glDeleteBuffers(1, *axis_vbo); glDeleteVertexArrays(1, *axis_vao); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } draw_scale :: (using plot: Plot, colors: PlotColors) { ar : float = cast(float) width / cast(float) height; content_x : s32 = x + xx left_bearing; content_y : s32; content_width : s32 = width - xx left_bearing; content_height : s32 = height - xx bottom_bearing; extent_x := xmax - xmin; extent_y := ymax - ymin; positions : [..]Vector2; defer array_reset(*positions); array_add(*positions, .{1.0, 1.0}); array_add(*positions, .{1.0 + content_width / 10.0, 1.0}); glViewport(0, 0, xx width, xx (height)); label_width := Simp.prepare_text(JetBrainsMonoRegular, tprint("%", formatFloat(cast(float) extent_x / zoom / 10.0, trailing_width = 1, zero_removal = .NO))); Simp.draw_prepared_text(JetBrainsMonoRegular, xx (5.0 + content_width / 10.0), xx (0), .{1, 1, 1, 1}); array_add(*positions, .{1.0, 1.0}); array_add(*positions, .{1.0, 1.0 + content_height / 10.0}); label_width = Simp.prepare_text(JetBrainsMonoRegular, tprint("%", formatFloat(cast(float) extent_y / zoom / 10.0, trailing_width = 1, zero_removal = .NO))); Simp.draw_prepared_text(JetBrainsMonoRegular, xx (0), xx (5.0 + content_height / 10.0), .{1, 1, 1, 1}); //glViewport(xx left_bearing, xx bottom_bearing, xx (width - left_bearing), xx (height - bottom_bearing)); scale_vao : GLuint; glGenVertexArrays(1, *scale_vao); glBindVertexArray(scale_vao); scale_vbo : GLuint; glGenBuffers(1, *scale_vbo); glBindBuffer(GL_ARRAY_BUFFER, scale_vbo); glBufferData(GL_ARRAY_BUFFER, positions.count * size_of(Vector2), positions.data, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, size_of(Vector2), null); proj := orthographic_projection_matrix(0.0, cast(float) (width), 0.0, cast(float) (height), -1.0, 1.0); glUseProgram(line_shader); glUniformMatrix4fv(glGetUniformLocation(line_shader, "proj"), 1, GL_TRUE, xx *proj); glUniform4f(glGetUniformLocation(line_shader, "data_color"), 1.0, 0.0, 0.0, 1.0); glDrawArrays(GL_LINES, 0, xx positions.count); primitives_rendered_this_frame += positions.count / 2; glDeleteBuffers(1, *scale_vbo); glDeleteVertexArrays(1, *scale_vao); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } is_in_rect :: (pos: Vector2, size: Vector2, test: Vector2) -> bool { top_left : Vector2 = pos; bottom_right : Vector2 = pos + size; if (test.x < top_left.x) return false; if (test.y < top_left.y) return false; if (test.x > bottom_right.x) return false; if (test.y > bottom_right.y) return false; return true; } data_shader : GLuint; line_shader : GLuint; make_shader :: (vertex_shader_text: string, fragment_shader_text: string) -> GLuint { success : s32; info : [512]u8; vertex_shader := glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, *vertex_shader_text.data, null); glCompileShader(vertex_shader); glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, *success); if !success { glGetShaderInfoLog(vertex_shader, 512, null, info.data); log("[ERROR] SHADER VERTEX COMPILATION_FAILED %", to_string(info.data)); } fragment_shader := glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment_shader, 1, *fragment_shader_text.data, null); glCompileShader(fragment_shader); glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, *success); if !success { glGetShaderInfoLog(fragment_shader, 512, null, info.data); log("[ERROR] SHADER VERTEX COMPILATION_FAILED %", to_string(info.data)); } shader := glCreateProgram(); glAttachShader(shader, vertex_shader); glAttachShader(shader, fragment_shader); glLinkProgram(shader); glGetProgramiv(shader, GL_LINK_STATUS, *success); if !success { glGetProgramInfoLog(shader, 512, null, info.data); log("[ERROR] SHADER PROGRAM LINKING_FAILED %", to_string(info.data)); } glDeleteShader(vertex_shader); glDeleteShader(fragment_shader); return shader; } /////////////////////////////////////////////////// // shader to render the lines representing the data /////////////////////////////////////////////////// data_vertex_shader_text : string = #string DONE #version 330 core layout (location = 0) in float data_x; layout (location = 1) in float data_y; uniform mat4 model; uniform mat4 proj; void main() { gl_Position = proj * model * vec4(data_x, data_y, 0.0, 1.0); } DONE data_fragment_shader_text : string = #string DONE #version 330 core uniform vec4 data_color; out vec4 color; float float_to_srgb(float l) { if (l < 0.0031308) { return l * 12.92; } else { return 1.055 * pow(l, 0.41666) - 0.055; } } void main() { color = vec4(data_color.rgb, float_to_srgb(data_color.a)); } DONE ////////////////////////////////////////////////////////// // general line shader for axis, squares, tick marks, etc. ////////////////////////////////////////////////////////// line_vertex_shader_text : string = #string DONE #version 330 core layout (location = 0) in vec2 data_pos; uniform mat4 proj; void main() { gl_Position = proj * vec4(data_pos.x, data_pos.y, 0.0, 1.0); } DONE line_fragment_shader_text : string = #string DONE #version 330 core uniform vec4 data_color; out vec4 color; float float_to_srgb(float l) { if (l < 0.0031308) { return l * 12.92; } else { return 1.055 * pow(l, 0.41666) - 0.055; } } void main() { color = vec4(data_color.rgb, float_to_srgb(data_color.a)); } DONE