Compare commits

...

22 Commits

Author SHA1 Message Date
a2fc2befd1 add cache for min/max calculations when drawing graphs 2025-04-08 21:38:54 +03:00
8d1cad16b3 add checkbox to toggle view sync controls 2025-04-08 21:38:54 +03:00
772f35fee7 add checkbox to toggle rulers 2025-04-08 21:38:54 +03:00
36706ff348 add synchronized movement between views 2025-04-08 21:38:54 +03:00
5c8a4ebb54 add back channel from device screen 2025-04-08 21:38:54 +03:00
ae26d2e1ba add custom draw callback to UI 2025-04-08 21:38:54 +03:00
23c4b99455 add sample rate input 2025-04-08 21:38:54 +03:00
293d220b34 add saving of project settings 2025-04-08 21:38:54 +03:00
d2b4942fa0 refactor in preparation for saving state to a file 2025-04-08 21:38:54 +03:00
c588956226 add preview for sample generation 2025-04-08 21:38:54 +03:00
31d0af0a5c add modal for protocol parameters 2025-04-08 21:38:53 +03:00
8ba3d0c914 add addition of channels from device screen 2025-03-19 01:58:11 +02:00
25377c8b4f add following of started collection 2025-03-16 14:21:25 +02:00
d8867cb3d6 show axis units on ruler 2025-03-16 13:38:53 +02:00
de2941c5bf add undo history to channel movements 2025-03-16 04:57:47 +02:00
859c36e93a add rulers to channel plots 2025-03-16 01:17:42 +02:00
6e332df183 show scientific numbers on ruler 2025-03-14 00:45:33 +02:00
778c0f4cb9 add fullscreen channel button 2025-03-12 20:00:27 +02:00
877f8034c7 rework UI 2025-03-10 01:01:13 +02:00
a6a66d99fd add basic markers to the sides of a channel view 2025-03-02 19:56:38 +02:00
e33ab321d0 add reset view button 2025-03-02 16:12:06 +02:00
09fccb7069 add zooming and panning using mouse in channel view 2025-03-02 16:02:04 +02:00
31 changed files with 6335 additions and 2692 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
.zig-cache
zig-out
.vscode
.vscode
profile.json

View File

@ -90,9 +90,20 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
});
const profiler_dep = b.dependency("profiler.zig", .{
.target = target,
.optimize = optimize,
});
const stb_image_lib = buildStbImage(b);
const cute_aseprite_lib = buildCuteAseprite(b, raylib_dep);
const generate_tool = b.addExecutable(.{
.name = "generate",
.root_source_file = b.path("tools/generate.zig"),
.target = target,
});
const png_to_icon_tool = b.addExecutable(.{
.name = "png-to-icon",
.root_source_file = b.path("tools/png-to-icon.zig"),
@ -114,6 +125,9 @@ pub fn build(b: *std.Build) !void {
exe.root_module.addImport("known-folders", known_folders);
exe.root_module.addImport("ini", ini);
// TODO: Add flag to disable in release
exe.root_module.addImport("profiler", profiler_dep.module("profiler"));
const external_compiler_support_dir = try std.process.getEnvVarOwned(b.allocator, "NIEXTCCOMPILERSUPP");
exe.addSystemIncludePath(.{ .cwd_relative = try std.fs.path.join(b.allocator, &.{ external_compiler_support_dir, "include" }) });
@ -134,22 +148,38 @@ pub fn build(b: *std.Build) !void {
}
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
{ // Run genration tool
const run_cmd = b.addRunArtifact(generate_tool);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("generate", "Run sample generation tool");
run_step.dependOn(&run_cmd.step);
}
const run_step = b.step("run", "Run the program");
run_step.dependOn(&run_cmd.step);
{ // Run main program
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_step = b.step("run", "Run the program");
run_step.dependOn(&run_cmd.step);
}
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
{ // Unit tests
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
}

View File

@ -11,10 +11,14 @@
.url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f",
.hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29",
},
.@"ini" = .{
.ini = .{
.url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz",
.hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8",
}
},
.@"profiler.zig" = .{
.url = "git+https://github.com/lassade/profiler.zig.git#d066d066c36c4eebd494babf15c1cdbd2d512b12",
.hash = "122097461acc2064f5f89b85d76d2a02232579864b17604617a333789c892f2d262f",
},
},
.paths = .{

File diff suppressed because it is too large Load Diff

View File

@ -5,15 +5,38 @@ const FontFace = @import("./font-face.zig");
const Aseprite = @import("cute_aseprite");
const assert = std.debug.assert;
const log = std.log.scoped(.assets);
pub const FontId = enum {
text
pub const FontVariant = enum {
regular,
regular_italic,
bold,
bold_italic,
thin,
thin_italic
};
var loaded_fonts: std.BoundedArray(rl.Font, 32) = .{};
pub const FontId = struct {
variant: FontVariant,
size: f32,
const FontArray = std.EnumArray(FontId, FontFace);
var fonts: FontArray = FontArray.initUndefined();
pub fn eql(self: FontId, other: FontId) bool {
return self.variant == other.variant and self.size == other.size;
}
};
const LoadedFont = struct {
id: FontId,
font: rl.Font,
generation: u64,
};
const FontTTFArray = std.EnumArray(FontVariant, []const u8);
var font_ttfs: FontTTFArray = undefined;
const LoadedFontsArray = std.BoundedArray(LoadedFont, std.meta.fields(FontVariant).len * 8);
var current_font_generation: u64 = 0;
var loaded_fonts: LoadedFontsArray = .{ };
pub var grab_texture: struct {
normal: rl.Texture2D,
@ -22,18 +45,53 @@ pub var grab_texture: struct {
} = undefined;
pub var dropdown_arrow: rl.Texture2D = undefined;
pub var fullscreen: rl.Texture2D = undefined;
pub var output_generation: rl.Texture2D = undefined;
pub var checkbox_mark: rl.Texture2D = undefined;
pub var cross: rl.Texture2D = undefined;
pub fn font(font_id: FontId) FontFace {
return fonts.get(font_id);
var found_font: ?LoadedFont = null;
for (loaded_fonts.slice()) |*loaded_font| {
if (font_id.eql(loaded_font.id)) {
loaded_font.generation = current_font_generation;
found_font = loaded_font.*;
}
}
if (found_font == null) {
const raylib_font = loadFont(
font_ttfs.get(font_id.variant),
@intFromFloat(@round(font_id.size))
) catch rl.getFontDefault();
found_font = LoadedFont{
.id = font_id,
.font = raylib_font,
.generation = current_font_generation
};
loaded_fonts.append(found_font.?) catch {
log.warn("Failed to append font, font cache is full", .{});
};
}
return FontFace{
.line_height = 1.2,
.font = found_font.?.font,
};
}
pub fn init(allocator: std.mem.Allocator) !void {
const roboto_regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf");
font_ttfs = FontTTFArray.init(.{
.regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf"),
.regular_italic = @embedFile("./assets/fonts/roboto/Roboto-Italic.ttf"),
const default_font = try loadFont(roboto_regular, 16);
.bold = @embedFile("./assets/fonts/roboto/Roboto-Bold.ttf"),
.bold_italic = @embedFile("./assets/fonts/roboto/Roboto-BoldItalic.ttf"),
fonts = FontArray.init(.{
.text = FontFace{ .font = default_font, .line_height = 1.2 }
.thin = @embedFile("./assets/fonts/roboto/Roboto-Thin.ttf"),
.thin_italic = @embedFile("./assets/fonts/roboto/Roboto-ThinItalic.ttf")
});
{
@ -62,16 +120,24 @@ pub fn init(allocator: std.mem.Allocator) !void {
};
}
{
const dropdown_arrow_ase = try Aseprite.init(allocator, @embedFile("./assets/dropdown-arrow.ase"));
defer dropdown_arrow_ase.deinit();
fullscreen = try loadTextureFromAseprite(allocator, @embedFile("./assets/dropdown-arrow.ase"));
fullscreen = try loadTextureFromAseprite(allocator, @embedFile("./assets/fullscreen-icon.ase"));
output_generation = try loadTextureFromAseprite(allocator, @embedFile("./assets/output-generation-icon.ase"));
checkbox_mark = try loadTextureFromAseprite(allocator, @embedFile("./assets/checkbox-mark.ase"));
cross = try loadTextureFromAseprite(allocator, @embedFile("./assets/cross.ase"));
}
const dropdown_image = dropdown_arrow_ase.getFrameImage(0);
defer dropdown_image.unload();
fn loadTextureFromAseprite(allocator: std.mem.Allocator, memory: []const u8) !rl.Texture {
const ase = try Aseprite.init(allocator, memory);
defer ase.deinit();
dropdown_arrow = rl.loadTextureFromImage(dropdown_image);
assert(rl.isTextureReady(dropdown_arrow));
}
const image = ase.getFrameImage(0);
defer image.unload();
const texture = rl.loadTextureFromImage(image);
assert(rl.isTextureReady(texture));
return texture;
}
fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font {
@ -86,22 +152,36 @@ fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font {
codepoints.appendAssumeCapacity(codepoint);
}
const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), codepoints.slice());
if (!loaded_font.isReady()) {
const raylib_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), codepoints.slice());
if (!raylib_font.isReady()) {
return error.LoadFontFromMemory;
}
loaded_fonts.appendAssumeCapacity(loaded_font);
return raylib_font;
}
return loaded_font;
pub fn deinitUnusedFonts() void {
var i: usize = 0;
while (i < loaded_fonts.len) {
const loaded_font = loaded_fonts.buffer[i];
if (loaded_font.generation < current_font_generation) {
loaded_font.font.unload();
_ = loaded_fonts.swapRemove(i);
} else {
i += 1;
}
}
current_font_generation += 1;
}
pub fn deinit(allocator: std.mem.Allocator) void {
_ = allocator;
for (loaded_fonts.slice()) |loaded_font| {
loaded_font.unload();
loaded_font.font.unload();
}
loaded_fonts.len = 0;
grab_texture.active.unload();
grab_texture.hot.unload();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,286 @@
const std = @import("std");
const App = @import("../../app.zig");
const UI = @import("../../ui.zig");
const RangeF64 = @import("../../range.zig").RangeF64;
const constants = @import("../../constants.zig");
const Id = App.Id;
const System = @This();
pub const ViewAxisPosition = struct {
view_id: Id,
axis: UI.Axis,
position: f64,
fn set(optional_self: *?ViewAxisPosition, view_id: Id, axis: UI.Axis, position: ?f64) void {
if (optional_self.*) |self| {
if (self.axis != axis or !self.view_id.eql(view_id)) {
return;
}
}
if (position != null) {
optional_self.* = .{
.axis = axis,
.position = position.?,
.view_id = view_id
};
} else {
optional_self.* = null;
}
}
fn get(optional_self: *?ViewAxisPosition, project: *App.Project, view_id: Id, axis: UI.Axis) ?f64 {
const self = optional_self.* orelse return null;
if (self.axis != axis) return null;
const view = project.views.get(view_id) orelse return null;
if (!view.sync_controls) {
if (!self.view_id.eql(view_id)) {
return null;
}
}
return self.position;
}
};
pub const Command = struct {
view_id: Id,
action: union(enum) {
move_and_zoom: struct {
before_x: RangeF64,
before_y: RangeF64,
x: RangeF64,
y: RangeF64
}
},
fn apply(self: *const Command, system: *System) void {
const project = system.project;
const view = project.views.get(self.view_id) orelse return;
switch (self.action) {
.move_and_zoom => |move_and_zoom| {
const view_rect = &view.graph_opts;
view_rect.x_range = move_and_zoom.x;
view_rect.y_range = move_and_zoom.y;
view.follow = false; // TODO: Undo follow
}
}
}
fn undo(self: *const Command, system: *System) void {
const project = system.project;
const view = project.views.get(self.view_id) orelse return;
switch (self.action) {
.move_and_zoom => |move_and_zoom| {
const view_rect = &view.graph_opts;
view_rect.x_range = move_and_zoom.before_x;
view_rect.y_range = move_and_zoom.before_y;
}
}
}
};
pub const CommandFrame = struct {
updated_at_ns: i128,
commands: std.BoundedArray(Command, constants.max_views) = .{},
fn findCommandByView(self: *CommandFrame, view_id: Id) ?*Command {
for (self.commands.slice()) |*command| {
if (command.view_id.eql(view_id)) {
return command;
}
}
return null;
}
fn apply(self: *const CommandFrame, system: *System) void {
const commands: []const Command = self.commands.constSlice();
for (commands) |*command| {
command.apply(system);
}
}
fn undo(self: *const CommandFrame, system: *System) void {
const commands: []const Command = self.commands.constSlice();
for (0..commands.len) |i| {
const command = commands[commands.len - 1 - i];
command.undo(system);
}
}
};
pub const CommandFrameArray = std.BoundedArray(CommandFrame, 64);
project: *App.Project,
// TODO: Redo
undo_stack: CommandFrameArray = .{},
last_applied_command: usize = 0,
zoom_start: ?ViewAxisPosition = null,
cursor: ?ViewAxisPosition = null,
view_settings: ?Id = null, // View id
view_fullscreen: ?Id = null, // View id
view_protocol_modal: ?Id = null, // View id
show_ruler: bool = false,
pub fn init(project: *App.Project) System {
return System{
.project = project
};
}
fn pushCommandFrame(self: *System) *CommandFrame {
if (self.undo_stack.unusedCapacitySlice().len == 0) {
_ = self.undo_stack.orderedRemove(0);
}
var frame = self.undo_stack.addOneAssumeCapacity();
frame.updated_at_ns = std.time.nanoTimestamp();
return frame;
}
fn lastCommandFrame(self: *System) ?*CommandFrame {
if (self.undo_stack.len > 0) {
return &self.undo_stack.buffer[self.undo_stack.len - 1];
}
return null;
}
pub fn pushViewMove(self: *System, view_id: Id, x_range: RangeF64, y_range: RangeF64) void {
var frame: *CommandFrame = undefined;
{
var push_new_command = true;
if (self.lastCommandFrame()) |last_frame| {
frame = last_frame;
const now_ns = std.time.nanoTimestamp();
if (now_ns - last_frame.updated_at_ns < std.time.ns_per_ms * 250) {
last_frame.updated_at_ns = now_ns;
push_new_command = false;
if (self.last_applied_command == self.undo_stack.len) {
self.last_applied_command -= 1;
}
}
}
if (push_new_command) {
frame = self.pushCommandFrame();
}
}
var sync_controls = false;
if (self.project.views.get(view_id)) |view| {
sync_controls = view.sync_controls;
}
var view_ids: std.BoundedArray(Id, constants.max_views) = .{};
if (sync_controls) {
var iter = self.project.views.idIterator();
while (iter.next()) |id| {
if (self.project.views.get(id).?.sync_controls) {
view_ids.appendAssumeCapacity(id);
}
}
} else {
view_ids.appendAssumeCapacity(view_id);
}
for (view_ids.constSlice()) |id| {
var command: *Command = undefined;
if (frame.findCommandByView(id)) |prev_command| {
command = prev_command;
command.action.move_and_zoom.x = x_range;
command.action.move_and_zoom.y = y_range;
} else {
const view = self.project.views.get(view_id) orelse continue;
const view_rect = &view.graph_opts;
command = frame.commands.addOneAssumeCapacity();
command.* = Command{
.view_id = id,
.action = .{
.move_and_zoom = .{
.before_x = view_rect.x_range,
.before_y = view_rect.y_range,
.x = x_range,
.y = y_range
}
}
};
}
}
}
pub fn pushViewMoveAxis(self: *System, view_id: Id, axis: UI.Axis, view_range: RangeF64) void {
const view = self.project.views.get(view_id) orelse return;
const view_rect = &view.graph_opts;
if (axis == .X) {
self.pushViewMove(view_id, view_range, view_rect.y_range);
} else {
self.pushViewMove(view_id, view_rect.x_range, view_range);
}
}
pub fn undoLastMove(self: *System) void {
const frame = self.undo_stack.popOrNull() orelse return;
frame.undo(self);
self.last_applied_command = @min(self.last_applied_command, self.undo_stack.len);
}
pub fn applyCommands(self: *System) void {
while (self.last_applied_command < self.undo_stack.len) : (self.last_applied_command += 1) {
const frame = self.undo_stack.buffer[self.last_applied_command];
frame.apply(self);
}
}
pub fn toggleViewSettings(self: *System, view_id: Id) void {
if (self.isViewSettingsOpen(view_id)) {
self.view_settings = null;
} else {
self.view_settings = view_id;
}
}
pub fn isViewSettingsOpen(self: *System, view_id: Id) bool {
return self.view_settings != null and self.view_settings.?.eql(view_id);
}
pub fn toggleFullscreenView(self: *System, view_id: Id) void {
if (self.view_fullscreen == null or !self.view_fullscreen.?.eql(view_id)) {
self.view_fullscreen = view_id;
} else {
self.view_fullscreen = null;
}
}
pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
return ViewAxisPosition.set(&self.cursor, view_id, axis, position);
}
pub fn getCursor(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
return ViewAxisPosition.get(&self.cursor, self.project, view_id, axis);
}
pub fn setZoomStart(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
return ViewAxisPosition.set(&self.zoom_start, view_id, axis, position);
}
pub fn getZoomStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
return ViewAxisPosition.get(&self.zoom_start, self.project,view_id, axis);
}

318
src/components/view.zig Normal file
View File

@ -0,0 +1,318 @@
const std = @import("std");
const App = @import("../app.zig");
const UI = @import("../ui.zig");
const srcery = @import("../srcery.zig");
const RangeF64 = @import("../range.zig").RangeF64;
const utils = @import("../utils.zig");
const NIDaq = @import("../ni-daq/root.zig");
const Assets = @import("../assets.zig");
const rl = @import("raylib");
const constants = @import("../constants.zig");
const Graph = @import("../graph.zig");
const UIViewRuler = @import("./view_ruler.zig");
const ViewControlsSystem = @import("./systems/view_controls.zig");
const Id = App.Id;
const remap = utils.remap;
const assert = std.debug.assert;
const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 });
pub const ZoomStart = UIViewRuler.ZoomStart;
pub const Context = struct {
ui: *UI,
app: *App,
view_controls: *ViewControlsSystem,
};
pub const Result = struct {
box: *UI.Box,
};
fn showGraph(ctx: Context, view_id: Id) *UI.Box {
var ui = ctx.ui;
const app = ctx.app;
const view = app.getView(view_id).?;
const samples = app.getViewSamples(view_id);
const view_opts = &view.graph_opts;
const graph_box = ui.createBox(.{
.key = ui.keyFromString("Graph"),
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
.background = srcery.black,
.flags = &.{ .clickable, .draggable, .scrollable, .clip_view },
.align_x = .center,
.align_y = .center,
.borders = .{
.bottom = .{ .color = srcery.hard_black, .size = 4 }
}
});
graph_box.beginChildren();
defer graph_box.endChildren();
const graph_rect = graph_box.rect();
const signal = ui.signal(graph_box);
var sample_value_under_mouse: ?f64 = null;
var sample_index_under_mouse: ?f64 = null;
const mouse_x_range = RangeF64.init(0, graph_rect.width);
const mouse_y_range = RangeF64.init(0, graph_rect.height);
if (signal.hot) {
sample_index_under_mouse = mouse_x_range.remapTo(view_opts.x_range, signal.relative_mouse.x);
sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y);
}
if (signal.dragged()) {
const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x);
const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y);
ctx.view_controls.pushViewMove(
view_id,
view_opts.x_range.sub(x_offset),
view_opts.y_range.add(y_offset)
);
}
if (signal.scrolled() and sample_index_under_mouse != null and sample_value_under_mouse != null) {
var scale_factor: f64 = 1;
if (signal.scroll.y > 0) {
scale_factor -= constants.zoom_speed;
} else {
scale_factor += constants.zoom_speed;
}
ctx.view_controls.pushViewMove(
view_id,
view_opts.x_range.zoom(sample_index_under_mouse.?, scale_factor),
view_opts.y_range.zoom(sample_value_under_mouse.?, scale_factor)
);
}
if (signal.flags.contains(.middle_clicked)) {
ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range);
}
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
if (view.graph_cache.texture) |texture| {
graph_box.texture = texture.texture;
}
if (view_opts.x_range.size() == 0 or view_opts.y_range.size() == 0) {
graph_box.setText("<Empty>");
graph_box.text_color = srcery.hard_black;
graph_box.font = .{
.variant = .bold_italic,
.size = ui.rem(3)
};
}
return graph_box;
}
fn showToolbar(ctx: Context, view_id: Id) void {
var ui = ctx.ui;
const toolbar = ui.createBox(.{
.layout_direction = .left_to_right,
.background = srcery.hard_black,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixed(.{ .pixels = ui.rem(2) })
});
toolbar.beginChildren();
defer toolbar.endChildren();
const view = ctx.app.getView(view_id).?;
var view_name: ?[]const u8 = null;
{
const btn = ui.textButton("Settings");
btn.background = srcery.hard_black;
if (ctx.view_controls.isViewSettingsOpen(view_id)) {
btn.borders.bottom = .{
.color = srcery.green,
.size = 4
};
}
if (ui.signal(btn).clicked()) {
ctx.view_controls.toggleViewSettings(view_id);
}
}
{
const btn = ui.textButton("Reset view");
btn.background = srcery.hard_black;
if (ui.signal(btn).clicked()) {
ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range);
}
}
if (view.reference == .channel) {
const channel_id = view.reference.channel;
const channel = ctx.app.getChannel(channel_id).?;
const channel_name = utils.getBoundedStringZ(&channel.name);
const channel_type = NIDaq.getChannelType(channel_name).?;
{
const follow = ui.textButton("Follow");
follow.background = srcery.hard_black;
if (view.follow) {
follow.borders = UI.Borders.bottom(.{
.color = srcery.green,
.size = 4
});
}
if (ui.signal(follow).clicked()) {
view.follow = !view.follow;
}
}
if (channel_type == .analog_output) {
const button = ui.button(ui.keyFromString("Output generation"));
button.texture = Assets.output_generation;
button.size.y = UI.Sizing.initGrowFull();
const signal = ui.signal(button);
if (signal.clicked()) {
if (ctx.app.isChannelOutputing(channel_id)) {
ctx.app.pushCommand(.{
.stop_output = channel_id
});
} else {
ctx.view_controls.view_protocol_modal = view_id;
}
}
var color = rl.Color.white;
if (ctx.app.isChannelOutputing(channel_id)) {
color = srcery.red;
}
if (signal.active) {
button.texture_color = color.alpha(0.6);
} else if (signal.hot) {
button.texture_color = color.alpha(0.8);
} else {
button.texture_color = color;
}
}
view_name = channel_name;
} else if (view.reference == .file) {
const file_id = view.reference.file;
const file = ctx.app.getFile(file_id).?;
view_name = std.fs.path.stem(file.path);
}
if (view.sync_controls) {
const btn = ui.button(ui.keyFromString("Disable sync"));
btn.texture = Assets.cross;
btn.size.y = UI.Sizing.initGrowFull();
btn.tooltip = "Disable sync controls";
if (ui.signal(btn).clicked()) {
view.sync_controls = false;
}
}
if (view_name) |text| {
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull()
});
const label = ui.label("{s}", .{text});
label.size.y = UI.Sizing.initGrowFull();
label.alignment.x = .center;
label.alignment.y = .center;
label.padding = UI.Padding.horizontal(ui.rem(1));
}
}
pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result {
var ui = ctx.ui;
const view_box = ui.createBox(.{
.key = UI.Key.initUsize(view_id.asInt()),
.layout_direction = .top_to_bottom,
.size_x = UI.Sizing.initGrowFull(),
.size_y = height,
});
view_box.beginChildren();
defer view_box.endChildren();
const result = Result{
.box = view_box
};
showToolbar(ctx, view_id);
if (!ctx.app.project.show_rulers) {
_ = showGraph(ctx, view_id);
} else {
const ruler_ctx = UIViewRuler.Context{
.ui = ctx.ui,
.project = &ctx.app.project,
.view_controls = ctx.view_controls
};
var graph_box: *UI.Box = undefined;
var x_ruler: *UI.Box = undefined;
var y_ruler: *UI.Box = undefined;
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
});
container.beginChildren();
defer container.endChildren();
y_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("Y ruler"), .Y);
graph_box = showGraph(ctx, view_id);
}
{
const container = ui.createBox(.{
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = ruler_size,
});
container.beginChildren();
defer container.endChildren();
const fullscreen = ui.createBox(.{
.key = ui.keyFromString("Fullscreen toggle"),
.size_x = ruler_size,
.size_y = ruler_size,
.background = srcery.hard_black,
.hot_cursor = .mouse_cursor_pointing_hand,
.flags = &.{ .draw_hot, .draw_active, .clickable },
.texture = Assets.fullscreen,
.texture_size = .{ .x = 28, .y = 28 }
});
if (ui.signal(fullscreen).clicked()) {
ctx.view_controls.toggleFullscreenView(view_id);
}
x_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("X ruler"), .X);
}
try UIViewRuler.show(ruler_ctx, x_ruler, graph_box, view_id, .X);
try UIViewRuler.show(ruler_ctx, y_ruler, graph_box, view_id, .Y);
}
return result;
}

View File

@ -0,0 +1,335 @@
const std = @import("std");
const rl = @import("raylib");
const App = @import("../app.zig");
const UI = @import("../ui.zig");
const RangeF64 = @import("../range.zig").RangeF64;
const srcery = @import("../srcery.zig");
const utils = @import("../utils.zig");
const constants = @import("../constants.zig");
const ViewControlsSystem = @import("./systems/view_controls.zig");
const Id = App.Id;
const remap = utils.remap;
const assert = std.debug.assert;
const ruler_size = UI.Sizing.initFixed(.{ .pixels = 32 });
pub const Context = struct {
ui: *UI,
project: *App.Project,
view_controls: *ViewControlsSystem
};
pub fn createBox(ctx: Context, key: UI.Key, axis: UI.Axis) *UI.Box {
var ui = ctx.ui;
var ruler = ui.createBox(.{
.key = key,
.background = srcery.hard_black,
.flags = &.{ .clickable, .scrollable, .clip_view },
.hot_cursor = .mouse_cursor_pointing_hand
});
if (axis == .X) {
ruler.size.x = UI.Sizing.initGrowFull();
ruler.size.y = ruler_size;
} else {
ruler.size.x = ruler_size;
ruler.size.y = UI.Sizing.initGrowFull();
}
return ruler;
}
const DrawContext = struct {
one_unit: ?f64,
render_range: RangeF64,
available_range: RangeF64,
axis: UI.Axis,
rect: rl.Rectangle = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
fn init(axis: UI.Axis, project: *App.Project, view_id: Id) DrawContext {
const view = project.views.get(view_id).?;
return DrawContext{
.one_unit = switch (axis) {
.X => project.getSampleRate(),
.Y => 1
},
.render_range = view.getGraphView(axis).*,
.available_range = view.getAvailableView(axis),
.axis = axis,
};
}
fn getPoint(self: *DrawContext, along_axis_pos: f64, cross_axis_pos: f64) rl.Vector2 {
const rect_width: f64 = @floatCast(self.rect.width);
const rect_height: f64 = @floatCast(self.rect.height);
var x: f64 = undefined;
var y: f64 = undefined;
if (self.axis == .X) {
x = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_width, along_axis_pos);
y = cross_axis_pos * rect_height;
} else {
assert(self.axis == .Y);
x = (1 - cross_axis_pos) * rect_width;
y = remap(f64, self.render_range.lower, self.render_range.upper, 0, rect_height, along_axis_pos);
}
return rl.Vector2{
.x = self.rect.x + @as(f32, @floatCast(x)),
.y = self.rect.y + @as(f32, @floatCast(y)),
};
}
fn getLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
const along_axis_size = self.render_range.size() / switch(self.axis) {
.X => @as(f64, @floatCast(self.rect.width)),
.Y => @as(f64, @floatCast(self.rect.height))
};
return self.getRect(
along_axis_pos,
along_axis_size,
0,
cross_axis_size
);
}
fn getRect(self: *DrawContext, along_axis_pos: f64, along_axis_size: f64, cross_axis_pos: f64, cross_axis_size: f64) rl.Rectangle {
const pos = self.getPoint(along_axis_pos, cross_axis_pos);
const corner = self.getPoint(along_axis_pos + along_axis_size, cross_axis_pos + cross_axis_size);
var rect = rl.Rectangle{
.x = pos.x,
.y = pos.y,
.width = corner.x - pos.x,
.height = corner.y - pos.y
};
if (rect.width < 0) {
rect.x += rect.width;
rect.width *= -1;
}
if (rect.height < 0) {
rect.y += rect.height;
rect.height *= -1;
}
return rect;
}
fn drawLine(self: *DrawContext, along_axis_pos: f64, cross_axis_size: f64, color: rl.Color) void {
rl.drawLineV(
self.getPoint(along_axis_pos, 0),
self.getPoint(along_axis_pos, cross_axis_size),
color
);
}
fn drawTicks(self: *DrawContext, from: f64, to: f64, step: f64, line_size: f64, color: rl.Color) void {
var position = from;
while (position < to) : (position += step) {
self.drawLine(position, line_size, color);
}
}
};
fn drawRulerTicks(_ctx: ?*anyopaque, box: *UI.Box) void {
const ctx: *DrawContext = @ptrCast(@alignCast(_ctx));
ctx.rect = box.rect();
ctx.drawLine(ctx.available_range.lower, 1, srcery.yellow);
ctx.drawLine(ctx.available_range.upper, 1, srcery.yellow);
if (ctx.available_range.hasExclusive(0)) {
ctx.drawLine(0, 0.75, srcery.yellow);
}
var one_unit = ctx.one_unit orelse return;
while (ctx.render_range.size() < 5*one_unit) {
one_unit /= 2;
}
while (ctx.render_range.size() > 50*one_unit) {
one_unit *= 2;
}
var ticks_range = ctx.render_range.grow(one_unit).intersectPositive(ctx.available_range);
ticks_range.lower = utils.roundNearestTowardZero(f64, ticks_range.lower, one_unit);
ctx.drawTicks(ticks_range.lower, ticks_range.upper, one_unit, 0.5, srcery.yellow);
ctx.drawTicks(ticks_range.lower + one_unit/2, ticks_range.upper, one_unit, 0.25, srcery.yellow);
}
fn showMouseTooltip(ctx: Context, axis: UI.Axis, view_id: Id, position: f64) void {
var ui = ctx.ui;
const view = ctx.project.views.get(view_id) orelse return;
const mouse_tooltip = ui.mouseTooltip();
mouse_tooltip.beginChildren();
defer mouse_tooltip.endChildren();
if (view.getAvailableView(axis).hasInclusive(position)) {
const sample_rate = ctx.project.getSampleRate();
if (axis == .Y and view.unit != null) {
const unit_name = view.unit.?.name() orelse "Unknown";
_ = ui.label("{s}: {d:.3}", .{unit_name, position});
} else if (axis == .X and sample_rate != null) {
const seconds = position / sample_rate.?;
const frame_allocator = ui.frameAllocator();
_ = ui.label("{s}", .{ utils.formatDuration(frame_allocator, seconds) catch "-" });
} else {
_ = ui.label("{d:.3}", .{position});
}
}
}
pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: UI.Axis) !void {
var ui = ctx.ui;
const project = ctx.project;
const view = project.views.get(view_id) orelse return;
var ruler_ctx = DrawContext.init(axis, project, view_id);
var graph_ctx = ruler_ctx;
ruler_ctx.rect = .{
.x = 0,
.y = 0,
.width = box.persistent.size.x,
.height = box.persistent.size.y
};
graph_ctx.rect = .{
.x = 0,
.y = 0,
.width = graph_box.persistent.size.x,
.height = graph_box.persistent.size.y
};
box.beginChildren();
defer box.endChildren();
{
const ruler_draw_ctx = try ui.frameAllocator().create(DrawContext);
ruler_draw_ctx.* = DrawContext.init(axis, project, view_id);
box.draw = .{
.ctx = ruler_draw_ctx,
.do = drawRulerTicks
};
}
{ // Visuals
const cursor = ctx.view_controls.getCursor(view_id, axis);
var zoom_start: ?f64 = null;
if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start_position| {
zoom_start = zoom_start_position;
}
var markers: std.BoundedArray(f64, 2) = .{};
if (zoom_start != null) {
markers.appendAssumeCapacity(zoom_start.?);
}
if (cursor != null) {
markers.appendAssumeCapacity(cursor.?);
}
for (markers.constSlice()) |marker_position| {
_ = ui.createBox(.{
.background = srcery.green,
.float_rect = ruler_ctx.getLine(marker_position, 1),
.float_relative_to = box,
});
_ = ui.createBox(.{
.background = srcery.green,
.float_rect = graph_ctx.getLine(marker_position, 1),
.float_relative_to = graph_box,
.parent = graph_box
});
}
if (zoom_start != null and cursor != null) {
const zoom_end = cursor.?;
_ = ui.createBox(.{
.background = srcery.green.alpha(0.5),
.float_relative_to = box,
.float_rect = ruler_ctx.getRect(zoom_start.?, zoom_end - zoom_start.?, 0, 1),
});
}
}
const signal = ui.signal(box);
const mouse_position = switch (axis) {
.X => signal.relative_mouse.x,
.Y => signal.relative_mouse.y
};
const mouse_range = switch (axis) {
.X => RangeF64.init(0, box.persistent.size.x),
.Y => RangeF64.init(0, box.persistent.size.y)
};
const view_range = view.getGraphView(axis);
if (signal.hot and view_range.size() > 0) {
const mouse_position_on_graph = mouse_range.remapTo(view_range.*, mouse_position);
showMouseTooltip(ctx, axis, view_id, mouse_position_on_graph);
ctx.view_controls.setCursor(view_id, axis, mouse_position_on_graph);
} else {
ctx.view_controls.setCursor(view_id, axis, null);
}
const cursor = ctx.view_controls.getCursor(view_id, axis);
if (signal.flags.contains(.left_pressed)) {
ctx.view_controls.setZoomStart(view_id, axis, cursor);
}
if (signal.scrolled() and cursor != null) {
var scale_factor: f64 = 1;
if (signal.scroll.y > 0) {
scale_factor -= constants.zoom_speed;
} else {
scale_factor += constants.zoom_speed;
}
const new_view_range = view_range.zoom(cursor.?, scale_factor);
ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range);
}
if (ctx.view_controls.getZoomStart(view_id, axis)) |zoom_start| {
if (signal.flags.contains(.left_released)) {
const zoom_end = cursor;
if (zoom_end != null) {
const zoom_start_mouse = view_range.remapTo(mouse_range, zoom_start);
const zoom_end_mouse = view_range.remapTo(mouse_range, zoom_end.?);
const mouse_move_distance = @abs(zoom_end_mouse - zoom_start_mouse);
if (mouse_move_distance > 5) {
var new_view_range = RangeF64.init(
@min(zoom_start, zoom_end.?),
@max(zoom_start, zoom_end.?)
);
if (axis == .Y) {
new_view_range = new_view_range.flip();
}
ctx.view_controls.pushViewMoveAxis(view_id, axis, new_view_range);
}
}
ctx.view_controls.zoom_start = null;
}
}
}

7
src/constants.zig Normal file
View File

@ -0,0 +1,7 @@
pub const max_files = 32;
pub const max_channels = 32;
pub const max_views = 64;
// UI
pub const zoom_speed = 0.1;

View File

@ -2,10 +2,73 @@ const std = @import("std");
const rl = @import("raylib");
const Allocator = std.mem.Allocator;
const FontFace = @This();
font: rl.Font,
spacing: ?f32 = null,
line_height: f32 = 1.4,
pub const DrawTextContext = struct {
font_face: FontFace,
tint: rl.Color,
origin: rl.Vector2,
offset: rl.Vector2 = .{ .x = 0, .y = 0 },
pub fn moveToNextLine(self: *DrawTextContext) void {
const font_size = self.font_face.getSize();
self.offset.x = 0;
self.offset.y += font_size * self.font_face.line_height;
}
pub fn advanceX(self: *DrawTextContext, ox: f32) void {
const font_size = self.font_face.getSize();
self.offset.x += ox * font_size;
}
pub fn advanceY(self: *DrawTextContext, oy: f32) void {
const font_size = self.font_face.getSize();
self.offset.y += oy * font_size;
}
pub fn drawCodepoint(self: *DrawTextContext, codepoint: u21) void {
if (codepoint == '\n') {
self.moveToNextLine();
} else {
const font = self.font_face.font;
const font_size = self.font_face.getSize();
const spacing = self.font_face.getSpacing();
if (!(codepoint <= 255 and std.ascii.isWhitespace(@intCast(codepoint)))) {
var codepoint_position = self.origin.add(self.offset);
codepoint_position.x = @round(codepoint_position.x);
codepoint_position.y = @round(codepoint_position.y);
rl.drawTextCodepoint(font, codepoint, codepoint_position, font_size, self.tint);
}
const index: u32 = @intCast(rl.getGlyphIndex(font, codepoint));
if (font.glyphs[index].advanceX != 0) {
self.offset.x += @floatFromInt(font.glyphs[index].advanceX);
} else {
self.offset.x += font.recs[index].width;
self.offset.x += @floatFromInt(font.glyphs[index].offsetX);
}
self.offset.x += spacing;
}
}
pub fn drawText(self: *DrawTextContext, text: []const u8) void {
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
self.drawCodepoint(codepoint);
}
}
};
pub fn getSpacing(self: @This()) f32 {
if (self.spacing) |spacing| {
return spacing;
@ -52,41 +115,16 @@ pub fn measureTextLines(self: @This(), lines: []const []const u8) rl.Vector2 {
return text_size;
}
pub fn drawTextEx(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color, offset: *rl.Vector2) void {
pub fn drawText(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void {
if (self.font.texture.id == 0) return;
const font_size = self.getSize();
const spacing = self.getSpacing();
var ctx = DrawTextContext{
.tint = tint,
.font_face = self,
.origin = position
};
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
if (codepoint == '\n') {
offset.x = 0;
offset.y += font_size * self.line_height;
} else {
if (!(codepoint <= 255 and std.ascii.isWhitespace(@intCast(codepoint)))) {
var codepoint_position = position.add(offset.*);
codepoint_position.x = @round(codepoint_position.x);
codepoint_position.y = @round(codepoint_position.y);
rl.drawTextCodepoint(self.font, codepoint, codepoint_position, font_size, tint);
}
const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint));
if (self.font.glyphs[index].advanceX != 0) {
offset.x += @floatFromInt(self.font.glyphs[index].advanceX);
} else {
offset.x += self.font.recs[index].width;
offset.x += @floatFromInt(self.font.glyphs[index].offsetX);
}
offset.x += spacing;
}
}
}
pub fn drawText(self: @This(), text: []const u8, position: rl.Vector2, tint: rl.Color) void {
var offset = rl.Vector2.init(0, 0);
self.drawTextEx(text, position, tint, &offset);
ctx.drawText(text);
}
pub fn drawTextAlloc(self: @This(), allocator: Allocator, comptime fmt: []const u8, args: anytype, position: rl.Vector2, tint: rl.Color) !void {
@ -96,20 +134,26 @@ pub fn drawTextAlloc(self: @This(), allocator: Allocator, comptime fmt: []const
self.drawText(text, position, tint);
}
pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
var text_size = rl.Vector2.zero();
pub const MeasureOptions = struct {
up_to_width: ?f32 = null,
last_codepoint_index: usize = 0
};
if (self.font.texture.id == 0) return text_size; // Security check
if (text.len == 0) return text_size;
pub fn measureTextEx(self: @This(), text: []const u8, opts: *MeasureOptions) rl.Vector2 {
var result = rl.Vector2.zero();
if (self.font.texture.id == 0) return result; // Security check
if (text.len == 0) return result;
const font_size = self.getSize();
const spacing = self.getSpacing();
var line_width: f32 = 0;
text_size.y = font_size;
result.y = font_size;
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
var text_size = result;
if (codepoint == '\n') {
text_size.y += font_size * self.line_height;
@ -120,18 +164,34 @@ pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
}
const index: u32 = @intCast(rl.getGlyphIndex(self.font, codepoint));
if (self.font.glyphs[index].advanceX != 0) {
line_width += @floatFromInt(self.font.glyphs[index].advanceX);
const glyph = self.font.glyphs[index];
if (glyph.advanceX != 0) {
line_width += @floatFromInt(glyph.advanceX);
} else {
line_width += self.font.recs[index].width;
line_width += @floatFromInt(self.font.glyphs[index].offsetX);
line_width += @floatFromInt(glyph.offsetX);
}
text_size.x = @max(text_size.x, line_width);
}
if (opts.up_to_width) |up_to_width| {
if (text_size.x > up_to_width) {
break;
}
}
opts.last_codepoint_index = iter.i;
result = text_size;
}
return text_size;
return result;
}
pub fn measureText(self: @This(), text: []const u8) rl.Vector2 {
var opts = MeasureOptions{};
return self.measureTextEx(text, &opts);
}
pub fn measureWidth(self: @This(), text: []const u8) f32 {

View File

@ -2,47 +2,144 @@ 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 {
from: f32, // inclusive
to: f32, // inclusive
min_value: f64,
max_value: f64,
left_aligned: bool = true,
x_range: RangeF64 = RangeF64.init(0, 0),
y_range: RangeF64 = RangeF64.init(0, 0),
color: rl.Color = srcery.red,
dot_size: f32 = 2
fn writeStruct(self: ViewOptions, writer: anytype) !void {
_ = self;
_ = writer;
// try writer.writeStructEndian(self.id, file_endian);
// try writer.writeStructEndian(self.channel_name.constSlice(), file_endian);
}
fn readStruct(reader: anytype) !ViewOptions {
_ = reader;
// const id = try reader.readStructEndian(Id, file_endian);
// const channel_name = try reader.readStructEndian([]const u8, file_endian);
return ViewOptions{
.x_range = RangeF64.init(0, 0),
.y_range = RangeF64.init(0, 0),
};
}
};
pub const Cache = struct {
texture: ?rl.RenderTexture2D = null,
options: ?ViewOptions = null,
pub const MinMaxCache = struct {
const MinMaxPair = struct {
min: f64,
max: f64
};
pub fn deinit(self: *Cache) void {
const chunk_size = 256;
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
sample_count: usize = 0,
pub fn deinit(self: *MinMaxCache, allocator: std.mem.Allocator) void {
self.min_max_pairs.clearAndFree(allocator);
self.sample_count = 0;
}
fn getMinMaxPair(chunk: []const f64) MinMaxPair {
assert(chunk.len > 0);
var min_sample = chunk[0];
var max_sample = chunk[0];
for (chunk) |sample| {
min_sample = @min(min_sample, sample);
max_sample = @max(max_sample, sample);
}
return MinMaxPair{
.min = min_sample,
.max = max_sample
};
}
pub fn updateAll(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
self.min_max_pairs.clearRetainingCapacity();
self.sample_count = 0;
if (samples.len == 0) return;
var iter = std.mem.window(f64, samples, chunk_size, chunk_size);
while (iter.next()) |chunk| {
try self.min_max_pairs.append(allocator, getMinMaxPair(chunk));
}
self.sample_count = samples.len;
}
pub fn updateLast(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
if (self.sample_count > samples.len) {
try self.updateAll(allocator, samples);
}
if (self.sample_count == samples.len) {
return;
}
const from_chunk = @divFloor(self.sample_count, chunk_size);
const to_chunk = @divFloor(samples.len - 1, chunk_size);
for (from_chunk..(to_chunk+1)) |i| {
const chunk = samples[
(chunk_size*i)..(@min(chunk_size*(i+1), samples.len))
];
const min_max_pair = getMinMaxPair(chunk);
if (i <= self.min_max_pairs.items.len) {
try self.min_max_pairs.append(allocator, min_max_pair);
} else {
self.min_max_pairs.items[i] = min_max_pair;
}
}
self.sample_count = samples.len;
}
};
pub const RenderCache = struct {
const Key = struct {
options: ViewOptions,
drawn_x_range: RangeF64
};
min_max_cache: ?MinMaxCache = null,
texture: ?rl.RenderTexture2D = null,
key: ?Key = null,
pub fn clear(self: *RenderCache) void {
if (self.texture) |texture| {
texture.unload();
self.texture = null;
}
self.options = null;
self.key = null;
}
pub fn invalidate(self: *Cache) void {
self.options = null;
pub fn invalidate(self: *RenderCache) void {
self.key = null;
}
pub fn draw(self: Cache, rect: rl.Rectangle) void {
pub fn draw(self: RenderCache, rect: rl.Rectangle) void {
if (self.texture) |texture| {
const source = rl.Rectangle{
.x = 0,
@ -61,137 +158,171 @@ pub const Cache = struct {
}
};
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 {
return remap(
f64,
view_rect.from, view_rect.to,
draw_rect.x, draw_rect.x + draw_rect.width,
index
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();
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewOptions, sample: f64) f64 {
return remap(
f64,
view_rect.min_value, view_rect.max_value,
draw_rect.y + draw_rect.height, draw_rect.y,
sample
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
var view_x_range = options.x_range;
view_x_range.upper += 2;
view_x_range.lower -= 1;
const i_range = view_x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
}
if (i_range.lower > i_range.upper) {
return;
}
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64, sample: f64) Vec2 {
return .{
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
};
}
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;
}
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 = mapSampleX(draw_rect, options, @floatFromInt(from_index));
const y_min = mapSampleY(draw_rect, options, column_min);
const y_max = mapSampleY(draw_rect, options, column_max);
if (column_samples.len == 1) {
rl.drawLineV(
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, options, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
options.color
);
rl.drawLineV(
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
mapSamplePointToGraph(draw_rect, options, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
options.color
);
} else if (@abs(y_max - y_min) < 1) {
rl.drawPixelV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
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),
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
);
defer rl.endScissorMode();
}
}
{
const from_index = clampIndexUsize(@floor(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
if (to_index - from_index > 0) {
for (from_index..(to_index-1)) |i| {
const from_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
const to_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i + 1), samples[i + 1]);
rl.drawLineV(from_point, to_point, options.color);
}
}
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 from_index = clampIndexUsize(@ceil(options.from), samples.len);
const to_index = clampIndexUsize(@ceil(options.to), samples.len);
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);
const min_circle_size = 0.5;
const max_circle_size = options.dot_size;
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
circle_size = @min(circle_size, max_circle_size);
for (from_index..to_index) |i| {
const center = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
rl.drawCircleV(center, circle_size, options.color);
}
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
);
}
}
}
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cache: MinMaxCache) 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(min_max_cache.sample_count)) - 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) {
var column_start: usize = @intFromFloat(i);
var column_end: usize = @intFromFloat(i + samples_per_column);
column_start = @divFloor(column_start, MinMaxCache.chunk_size);
column_end = @divFloor(column_end, MinMaxCache.chunk_size);
const min_max_pairs = min_max_cache.min_max_pairs.items[column_start..column_end];
if (min_max_pairs.len == 0) continue;
var column_min = min_max_pairs[0].min;
var column_max = min_max_pairs[0].max;
for (min_max_pairs) |min_max_pair| {
column_min = @min(column_min, min_max_pair.min);
column_max = @max(column_max, min_max_pair.max);
}
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, min_max_level: ?MinMaxCache) 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) {
if (min_max_level != null and samples_per_column > 2*MinMaxCache.chunk_size) {
drawSamplesMinMax(draw_rect, options, min_max_level.?);
} else {
drawSamplesApproximate(draw_rect, options, samples);
}
} else {
drawSamplesExact(draw_rect, options, samples);
}
}
pub fn drawCached(cache: *RenderCache, 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));
@ -205,7 +336,7 @@ pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, sample
if (texure.width != render_width or texure.height != render_height) {
render_texture.unload();
cache.texture = null;
cache.options = null;
cache.key = null;
}
}
@ -218,12 +349,17 @@ pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, sample
const render_texture = cache.texture.?;
if (cache.options != null and std.meta.eql(cache.options.?, options)) {
const cache_key = RenderCache.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.options = options;
cache.key = cache_key;
render_texture.begin();
defer render_texture.end();
@ -241,10 +377,10 @@ pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, sample
.width = render_size.x,
.height = render_size.y
};
drawSamples(draw_rect, options, samples);
drawSamples(draw_rect, options, samples, cache.min_max_cache);
}
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
if (draw_rect.width < 0 or draw_rect.height < 0) {
return;
}
@ -254,6 +390,6 @@ pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, sampl
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
c.draw(draw_rect);
} else {
drawSamples(draw_rect, options, samples);
drawSamples(draw_rect, options, samples, null);
}
}

View File

@ -3,7 +3,7 @@ const rl = @import("raylib");
const builtin = @import("builtin");
const Application = @import("./app.zig");
const Assets = @import("./assets.zig");
const Profiler = @import("./profiler.zig");
const Profiler = @import("./my-profiler.zig");
const Platform = @import("./platform.zig");
const raylib_h = @cImport({
@cInclude("stdio.h");
@ -71,71 +71,17 @@ pub fn main() !void {
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true
}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
// const devices = try ni_daq.listDeviceNames();
// for (devices) |device| {
// if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
// const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
// assert(voltage_ranges.len > 0);
// const min_sample = voltage_ranges[0].low;
// const max_sample = voltage_ranges[0].high;
// for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| {
// var channel = try app.appendChannel();
// channel.min_sample = min_sample;
// channel.max_sample = max_sample;
// try app.task_pool.createAIVoltageChannel(ni_daq, .{
// .channel = channel_name,
// .min_value = min_sample,
// .max_value = max_sample,
// });
// break;
// }
// }
// if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
// const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device);
// assert(voltage_ranges.len > 0);
// const min_sample = voltage_ranges[0].low;
// const max_sample = voltage_ranges[0].high;
// for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| {
// var channel = try app.appendChannel();
// channel.min_sample = min_sample;
// channel.max_sample = max_sample;
// try app.task_pool.createAOVoltageChannel(ni_daq, .{
// .channel = channel_name,
// .min_value = min_sample,
// .max_value = max_sample,
// });
// }
// }
// }
// for (0.., app.channels.items) |i, *channel| {
// channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8);
// }
// const sample_rate: f64 = 5000;
// try app.task_pool.setContinousSampleRate(sample_rate);
// var channel_samples = try app.task_pool.start(0.01, allocator);
// defer channel_samples.deinit();
// defer app.task_pool.stop() catch @panic("stop task failed");
// app.channel_samples = channel_samples;
const icon_png = @embedFile("./assets/icon.png");
var icon_image = rl.loadImageFromMemory(".png", icon_png);
defer icon_image.unload();
rl.initWindow(800, 450, "DAQ view");
rl.initWindow(800, 600, "DAQ view");
defer rl.closeWindow();
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
rl.setWindowMinSize(256, 256);
@ -156,9 +102,55 @@ pub fn main() !void {
defer app.deinit();
if (builtin.mode == .Debug) {
_ = try app.addView(.{
.channel = try app.addChannel("Dev1/ai0")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-5k.bin")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-50k.bin")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-300k.bin")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-9m.bin")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-18m.bin")
});
// _ = try app.addView(.{
// .channel = try app.addChannel("Dev1/ai0")
// });
// _ = try app.addView(.{
// .channel = try app.addChannel("Dev3/ao0")
// });
// _ = try app.addView(.{
// .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin")
// });
var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff);
const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" });
errdefer allocator.free(allocator);
app.project.save_location = save_location;
app.project.sample_rate = 5000;
// try app.appendChannelFromDevice("Dev1/ai0");
// try app.appendChannelFromDevice("Dev3/ao0");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
}
var profiler: ?Profiler = null;
@ -166,33 +158,51 @@ pub fn main() !void {
var profiler_shown = false;
if (profiler_enabled) {
const font_face = Assets.font(.text);
profiler = try Profiler.init(allocator, 10 * target_fps, @divFloor(std.time.ns_per_s, target_fps), font_face);
profiler = try Profiler.init(
allocator,
10 * target_fps,
@divFloor(std.time.ns_per_s, target_fps),
Assets.FontId{ .variant = .regular, .size = 16 }
);
}
while (!rl.windowShouldClose()) {
rl.setExitKey(rl.KeyboardKey.key_null);
var last_font_cleanup_at = rl.getTime();
while (!rl.windowShouldClose() and !app.should_close) {
rl.beginDrawing();
defer rl.endDrawing();
if (profiler) |*p| {
p.start();
}
try app.tick();
if (profiler) |*p| {
p.stop();
if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) {
profiler_shown = !profiler_shown;
{
if (profiler) |*p| {
p.start();
}
if (profiler_shown) {
try p.showResults();
try app.tick();
if (profiler) |*p| {
p.stop();
if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) {
profiler_shown = !profiler_shown;
}
if (profiler_shown) {
try p.showResults();
}
}
const now = rl.getTime();
if (now - last_font_cleanup_at > 10) {
Assets.deinitUnusedFonts();
last_font_cleanup_at = now;
}
}
rl.endDrawing();
}
}
test {
_ = @import("./ni-daq/root.zig");
_ = @import("./range.zig");
}

View File

@ -1,9 +1,10 @@
const std = @import("std");
const rl = @import("raylib");
const Assets = @import("./assets.zig");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const FontId = Assets.FontId;
const FontFace = @import("./font-face.zig");
const srcery = @import("./srcery.zig");
const rect_utils = @import("./rect-utils.zig");
@ -14,9 +15,9 @@ history_head: usize,
started_at: i128,
ns_budget: u128,
font_face: FontFace,
font: FontId,
pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font_face: FontFace) !@This() {
pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font: FontId) !@This() {
return @This(){
.allocator = allocator,
.history = try allocator.alloc(u128, data_points),
@ -24,7 +25,7 @@ pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font_face
.history_head = 0,
.started_at = 0,
.ns_budget = ns_budget,
.font_face = font_face,
.font = font,
};
}
@ -90,7 +91,8 @@ pub fn showResults(self: *const @This()) !void {
var layout_offset: f32 = 0;
const allocator = self.allocator;
const font_size = self.font_face.getSize();
const font_face = Assets.font(self.font);
const font_size = font_face.getSize();
const labels = .{
.{ "Min", @as(f32, @floatFromInt(min_time_taken)) },
@ -104,7 +106,7 @@ pub fn showResults(self: *const @This()) !void {
const min_time_str = try std.fmt.allocPrintZ(allocator, "{s}: {d:10.0}us ({d:.3}%)", .{ label_name, time_taken / std.time.ns_per_us, time_taken / ns_budget * 100 });
defer allocator.free(min_time_str);
self.font_face.drawText(min_time_str, .{ .x = content_rect.x, .y = content_rect.y + layout_offset }, color);
font_face.drawText(min_time_str, .{ .x = content_rect.x, .y = content_rect.y + layout_offset }, color);
layout_offset += font_size;
}
}

View File

@ -46,6 +46,13 @@ DAQmxGetDevProductCategory: *const @TypeOf(c.DAQmxGetDevProductCategory),
DAQmxGetDevAIPhysicalChans: *const @TypeOf(c.DAQmxGetDevAIPhysicalChans),
DAQmxGetDevAOPhysicalChans: *const @TypeOf(c.DAQmxGetDevAOPhysicalChans),
DAQmxReadAnalogF64: *const @TypeOf(c.DAQmxReadAnalogF64),
DAQmxWriteAnalogF64: *const @TypeOf(c.DAQmxWriteAnalogF64),
DAQmxGetWriteCurrWritePos: *const @TypeOf(c.DAQmxGetWriteCurrWritePos),
DAQmxGetWriteSpaceAvail: *const @TypeOf(c.DAQmxGetWriteSpaceAvail),
DAQmxCreateAOVoltageChan: *const @TypeOf(c.DAQmxCreateAOVoltageChan),
DAQmxGetWriteTotalSampPerChanGenerated: *const @TypeOf(c.DAQmxGetWriteTotalSampPerChanGenerated),
DAQmxCfgOutputBuffer: *const @TypeOf(c.DAQmxCfgOutputBuffer),
DAQmxCreateAOFuncGenChan: *const @TypeOf(c.DAQmxCreateAOFuncGenChan),
pub fn init() Error!Api {
var api: Api = undefined;

View File

@ -5,7 +5,7 @@ pub const c = Api.c;
const assert = std.debug.assert;
const log = std.log.scoped(.ni_daq);
const max_device_name_size = 255;
pub const max_device_name_size = 255;
const max_task_name_size = 255;
pub const max_channel_name_size = count: {
@ -86,10 +86,28 @@ pub const Task = struct {
);
}
pub fn setContinousSampleRate(self: Task, sample_rate: f64) !void {
pub const ContinousSamplingOptions = struct {
pub const Edge = enum(i32) {
rising = c.DAQmx_Val_Rising,
falling = c.DAQmx_Val_Falling
};
sample_rate: f64,
active_edge: Edge = .rising,
buffer_size: u64 = 0
};
pub fn setContinousSampleRate(self: Task, opts: ContinousSamplingOptions) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_ContSamps, 0),
api.DAQmxCfgSampClkTiming(
self.handle,
null,
opts.sample_rate,
@intFromEnum(opts.active_edge),
c.DAQmx_Val_ContSamps,
opts.buffer_size
),
error.DAQmxCfgSampClkTiming
);
}
@ -97,33 +115,63 @@ pub const Task = struct {
pub fn setFiniteSampleRate(self: Task, sample_rate: f64, samples_per_channel: u64) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgSampClkTiming(self.handle, null, sample_rate, c.DAQmx_Val_Rising, c.DAQmx_Val_FiniteSamps, samples_per_channel),
api.DAQmxCfgSampClkTiming(
self.handle,
null,
sample_rate,
c.DAQmx_Val_Rising,
c.DAQmx_Val_FiniteSamps,
samples_per_channel
),
error.DAQmxCfgSampClkTiming
);
}
pub const AIVoltageChannelOptions = struct {
pub const TerminalConfig = enum(i32) {
default = c.DAQmx_Val_Cfg_Default,
rse = c.DAQmx_Val_RSE,
nrse = c.DAQmx_Val_NRSE,
diff = c.DAQmx_Val_Diff,
pseudo_diff = c.DAQmx_Val_PseudoDiff,
};
channel: [:0]const u8,
assigned_name: [*c]const u8 = null,
terminal_config: i32 = c.DAQmx_Val_Cfg_Default,
terminal_config: TerminalConfig = .default,
min_value: f64,
max_value: f64,
units: i32 = c.DAQmx_Val_Volts,
custom_scale_name: [*c]const u8 = null,
units: union(enum) {
volts,
custom_scale: [*]const u8
} = .volts
};
pub fn createAIVoltageChannel(self: Task, options: AIVoltageChannelOptions) !void {
const api = self.ni_daq.api;
var custom_scale_name: [*c]const u8 = null;
var units: i32 = undefined;
switch (options.units) {
.volts => {
units = c.DAQmx_Val_Volts;
},
.custom_scale => |_custom_scale_name| {
units = c.DAQmx_Val_FromCustomScale;
custom_scale_name = _custom_scale_name;
}
}
try self.ni_daq.checkDAQmxError(
api.DAQmxCreateAIVoltageChan(
self.handle,
options.channel,
options.assigned_name,
options.terminal_config,
@intFromEnum(options.terminal_config),
options.min_value,
options.max_value,
options.units,
options.custom_scale_name
units,
custom_scale_name
),
error.DAQmxCreateAIVoltageChan
);
@ -134,25 +182,74 @@ pub const Task = struct {
assigned_name: [*c]const u8 = null,
min_value: f64,
max_value: f64,
units: i32 = c.DAQmx_Val_Volts,
custom_scale_name: [*c]const u8 = null,
units: union(enum) {
volts,
custom_scale: [*]const u8
} = .volts
};
pub fn createAOVoltageChannel(self: Task, options: AOVoltageChannelOptions) !void {
var custom_scale_name: [*c]const u8 = null;
var units: i32 = undefined;
switch (options.units) {
.volts => {
units = c.DAQmx_Val_Volts;
},
.custom_scale => |_custom_scale_name| {
units = c.DAQmx_Val_FromCustomScale;
custom_scale_name = _custom_scale_name;
}
}
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
c.DAQmxCreateAOVoltageChan(
api.DAQmxCreateAOVoltageChan(
self.handle,
options.channel,
options.assigned_name,
options.min_value,
options.max_value,
options.units,
options.custom_scale_name
units,
custom_scale_name
),
error.DAQmxCreateAOVoltageChan
);
}
pub const AOGenerationChannelOptions = struct {
pub const Function = enum(i32) {
sine = c.DAQmx_Val_Sine,
triangle = c.DAQmx_Val_Triangle,
square = c.DAQmx_Val_Square,
Sawtooth = c.DAQmx_Val_Sawtooth,
_
};
channel: [:0]const u8,
assigned_name: [*c]const u8 = null,
function: Function,
frequency: f64,
amplitude: f64,
offset: f64 = 0
};
pub fn createAOGenerationChannel(self: Task, options: AOGenerationChannelOptions) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCreateAOFuncGenChan(
self.handle,
options.channel,
options.assigned_name,
@intFromEnum(options.function),
options.frequency,
options.amplitude,
options.offset
),
error.DAQmxCreateAOFuncGenChan
);
}
pub const ReadAnalogOptions = struct {
read_array: []f64,
timeout: f64,
@ -177,7 +274,7 @@ pub const Task = struct {
if (err == c.DAQmxErrorSamplesNoLongerAvailable) {
self.dropped_samples += 1;
log.err("Dropped samples, not reading samples fast enough.", .{});
log.warn("Dropped samples, not reading samples fast enough.", .{});
} else if (err < 0) {
try self.ni_daq.checkDAQmxError(err, error.DAQmxReadAnalogF64);
}
@ -185,17 +282,84 @@ pub const Task = struct {
return @intCast(samples_per_channel);
}
pub const WriteAnalogOptions = struct {
write_array: []f64,
samples_per_channel: u32,
timeout: f64,
data_layout: u32 = c.DAQmx_Val_GroupByChannel,
auto_start: bool = false
};
pub fn writeAnalog(self: Task, options: WriteAnalogOptions) !u32 {
const api = self.ni_daq.api;
var samples_per_channel_written: i32 = 0;
const err = api.DAQmxWriteAnalogF64(
self.handle,
@intCast(options.samples_per_channel),
@intFromBool(options.auto_start),
options.timeout,
options.data_layout,
options.write_array.ptr,
&samples_per_channel_written,
null
);
try self.ni_daq.checkDAQmxError(err, error.DAQmxWriteAnalogF64);
return @intCast(samples_per_channel_written);
}
pub fn writeSpaceAvailable(self: Task) !u32 {
const api = self.ni_daq.api;
var amount: u32 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteSpaceAvail(self.handle, &amount),
error.DAQmxGetWriteSpaceAvail
);
return amount;
}
pub fn writeCurrentPosition(self: Task) !u64 {
const api = self.ni_daq.api;
var amount: u64 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteCurrWritePos(self.handle, &amount),
error.DAQmxGetWriteCurrWritePos
);
return amount;
}
pub fn writeTotalSamplesPerChannelGenerated(self: Task) !u64 {
const api = self.ni_daq.api;
var amount: u64 = 0;
try self.ni_daq.checkDAQmxError(
api.DAQmxGetWriteTotalSampPerChanGenerated(self.handle, &amount),
error.DAQmxGetWriteTotalSampPerChanGenerated
);
return amount;
}
pub fn configureOutputBuffer(self: Task, samples_per_channel: u32) !void {
const api = self.ni_daq.api;
try self.ni_daq.checkDAQmxError(
api.DAQmxCfgOutputBuffer(self.handle, samples_per_channel),
error.DAQmxCfgOutputBuffer
);
}
pub fn isDone(self: Task) !bool {
const api = self.ni_daq.api;
var result: c.bool32 = 0;
try self.ni_daq.checkDAQmxError(
c.DAQmxIsTaskDone(self.handle, &result),
api.DAQmxIsTaskDone(self.handle, &result),
error.DAQmxIsTaskDone
);
return result != 0;
}
};
pub const AIMeasurementType = enum(i32) {
pub const Unit = enum(i32) {
Voltage = c.DAQmx_Val_Voltage,
VoltageRMS = c.DAQmx_Val_VoltageRMS,
Current = c.DAQmx_Val_Current,
@ -227,7 +391,7 @@ pub const AIMeasurementType = enum(i32) {
Power = c.DAQmx_Val_Power,
_,
pub fn name(self: AIMeasurementType) []const u8 {
pub fn name(self: Unit) ?[]const u8 {
return switch (self) {
.Voltage => "Voltage",
.VoltageRMS => "Voltage RMS",
@ -258,11 +422,49 @@ pub const AIMeasurementType = enum(i32) {
.TEDS_Sensor => "TEDS",
.Charge => "Charge",
.Power => "Power source",
_ => "Unknown"
_ => null
};
}
};
pub const AIMeasurementType = enum(i32) {
Voltage = @intFromEnum(Unit.Voltage),
VoltageRMS = @intFromEnum(Unit.VoltageRMS),
Current = @intFromEnum(Unit.Current),
CurrentRMS = @intFromEnum(Unit.CurrentRMS),
Voltage_CustomWithExcitation = @intFromEnum(Unit.Voltage_CustomWithExcitation),
Bridge = @intFromEnum(Unit.Bridge),
Freq_Voltage = @intFromEnum(Unit.Freq_Voltage),
Resistance = @intFromEnum(Unit.Resistance),
Temp_TC = @intFromEnum(Unit.Temp_TC),
Temp_Thrmstr = @intFromEnum(Unit.Temp_Thrmstr),
Temp_RTD = @intFromEnum(Unit.Temp_RTD),
Temp_BuiltInSensor = @intFromEnum(Unit.Temp_BuiltInSensor),
Strain_Gage = @intFromEnum(Unit.Strain_Gage),
Rosette_Strain_Gage = @intFromEnum(Unit.Rosette_Strain_Gage),
Position_LVDT = @intFromEnum(Unit.Position_LVDT),
Position_RVDT = @intFromEnum(Unit.Position_RVDT),
Position_EddyCurrentProximityProbe = @intFromEnum(Unit.Position_EddyCurrentProximityProbe),
Accelerometer = @intFromEnum(Unit.Accelerometer),
Acceleration_Charge = @intFromEnum(Unit.Acceleration_Charge),
Acceleration_4WireDCVoltage = @intFromEnum(Unit.Acceleration_4WireDCVoltage),
Velocity_IEPESensor = @intFromEnum(Unit.Velocity_IEPESensor),
Force_Bridge = @intFromEnum(Unit.Force_Bridge),
Force_IEPESensor = @intFromEnum(Unit.Force_IEPESensor),
Pressure_Bridge = @intFromEnum(Unit.Pressure_Bridge),
SoundPressure_Microphone = @intFromEnum(Unit.SoundPressure_Microphone),
Torque_Bridge = @intFromEnum(Unit.Torque_Bridge),
TEDS_Sensor = @intFromEnum(Unit.TEDS_Sensor),
Charge = @intFromEnum(Unit.Charge),
Power = @intFromEnum(Unit.Power),
_,
pub fn name(self: AIMeasurementType) ?[]const u8 {
return Unit.name(self);
}
};
pub const max_ai_measurement_type_list_len = @typeInfo(AIMeasurementType).Enum.fields.len;
pub const AIMeasurementTypeList = std.BoundedArray(AIMeasurementType, max_ai_measurement_type_list_len);

View File

@ -1,171 +0,0 @@
const std = @import("std");
const NIDaq = @import("./root.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.task_pool);
const TaskPool = @This();
const max_tasks = 32;
pub const Sampling = union(enum) {
finite: struct {
sample_rate: f64,
sample_count: u64
},
continous: struct {
sample_rate: f64
}
};
pub const Entry = struct {
task: NIDaq.Task,
in_use: bool = false,
running: bool = false,
started_sampling_ns: i128,
stopped_sampling_ns: ?i128 = null,
dropped_samples: u32 = 0,
sampling: Sampling,
mutex: *std.Thread.Mutex,
samples: *std.ArrayList(f64),
pub fn stop(self: *Entry) !void {
self.running = false;
if (self.in_use) {
try self.task.stop();
self.task.clear();
}
self.in_use = false;
}
};
running: bool = false,
read_thread: std.Thread,
entries: [max_tasks]Entry = undefined,
pub fn init(self: *TaskPool, allocator: std.mem.Allocator) !void {
self.* = TaskPool{
.read_thread = undefined
};
self.running = true;
self.read_thread = try std.Thread.spawn(
.{ .allocator = allocator },
readThreadCallback,
.{ self }
);
for (&self.entries) |*entry| {
entry.in_use = false;
}
}
pub fn deinit(self: *TaskPool) void {
for (&self.entries) |*entry| {
entry.stop() catch log.err("Failed to stop entry", .{});
}
self.running = false;
self.read_thread.join();
}
fn readAnalog(entry: *Entry, timeout: f64) !void {
if (!entry.in_use) return;
if (!entry.running) return;
entry.mutex.lock();
defer entry.mutex.unlock();
switch (entry.sampling) {
.finite => |args| {
try entry.samples.ensureTotalCapacity(args.sample_count);
},
.continous => |args| {
try entry.samples.ensureUnusedCapacity(@intFromFloat(@ceil(args.sample_rate)));
}
}
const unused_capacity = entry.samples.unusedCapacitySlice();
if (unused_capacity.len == 0) return;
const read_amount = try entry.task.readAnalog(.{
.timeout = timeout,
.read_array = unused_capacity,
});
if (read_amount == 0) return;
entry.samples.items.len += read_amount;
}
fn readThreadCallback(task_pool: *TaskPool) void {
const timeout = 0.05;
while (task_pool.running) {
for (&task_pool.entries) |*entry| {
readAnalog(entry, timeout) catch |e| {
log.err("readAnalog() failed in thread: {}", .{e});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
entry.stop() catch log.err("failed to stop collecting", .{});
};
}
std.time.sleep(0.05 * std.time.ns_per_s);
}
}
fn findFreeEntry(self: *TaskPool) ?*Entry {
for (&self.entries) |*entry| {
if (!entry.in_use) {
return entry;
}
}
return null;
}
pub fn launchAIVoltageChannel(
self: *TaskPool,
ni_daq: *NIDaq,
mutex: *std.Thread.Mutex,
samples: *std.ArrayList(f64),
sampling: Sampling,
options: NIDaq.Task.AIVoltageChannelOptions
) !*Entry {
const task = try ni_daq.createTask(null);
errdefer task.clear();
const entry = self.findFreeEntry() orelse return error.NotEnoughSpace;
errdefer entry.in_use = false;
try task.createAIVoltageChannel(options);
switch (sampling) {
.continous => |args| {
try task.setContinousSampleRate(args.sample_rate);
},
.finite => |args| {
try task.setFiniteSampleRate(args.sample_rate, args.sample_count);
}
}
samples.clearRetainingCapacity();
try task.start();
const started_at = std.time.nanoTimestamp();
entry.* = Entry{
.task = task,
.started_sampling_ns = started_at,
.in_use = true,
.running = true,
.mutex = mutex,
.samples = samples,
.sampling = sampling,
};
return entry;
}

View File

@ -89,7 +89,7 @@ pub fn toggleConsoleWindow() void {
// TODO: Maybe return the file path instead of an opened file handle?
// So the user of this function could do something more interesting.
pub fn openFilePicker() !std.fs.File {
pub fn openFilePicker(allocator: std.mem.Allocator) ![]u8 {
if (builtin.os.tag != .windows) {
return error.NotSupported;
}
@ -125,13 +125,7 @@ pub fn openFilePicker() !std.fs.File {
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
const filename_w = filename_w_buffer[0..filename_len];
var filename_buffer: [std.fs.max_path_bytes]u8 = undefined;
// It should be safe to do "catch unreachable" here because `filename_buffer` will always be big enough.
const filename = std.fmt.bufPrint(&filename_buffer, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)}) catch unreachable;
// TODO: Use the `openFileAbsoluteW` function.
// Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error
return try std.fs.openFileAbsolute(filename, .{ });
return try std.fmt.allocPrint(allocator, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)});
}
pub fn init() void {

196
src/range.zig Normal file
View File

@ -0,0 +1,196 @@
const std = @import("std");
const rl = @import("raylib");
const remap_number = @import("./utils.zig").remap;
const assert = std.debug.assert;
pub fn Range(Number: type) type {
return packed struct {
const Self = @This();
lower: Number,
upper: Number,
pub fn init(lower: Number, upper: Number) Self {
return Self{
.lower = lower,
.upper = upper
};
}
pub fn initRect(rect: rl.Rectangle) [2]Self {
return .{
initRectX(rect),
initRectY(rect)
};
}
pub fn initRectX(rect: rl.Rectangle) Self {
return init(rect.x, rect.x + rect.width);
}
pub fn initRectY(rect: rl.Rectangle) Self {
return init(rect.y, rect.y + rect.height);
}
pub fn flip(self: Self) Self {
return init(self.upper, self.lower);
}
pub fn size(self: Self) Number {
return @abs(self.upper - self.lower);
}
pub fn hasExclusive(self: Self, number: Number) bool {
var upper = self.upper;
var lower = self.lower;
if (self.lower > self.upper) {
lower = self.upper;
upper = self.lower;
}
assert(lower <= upper);
return lower < number and number < upper;
}
pub fn hasInclusive(self: Self, number: Number) bool {
var upper = self.upper;
var lower = self.lower;
if (self.lower > self.upper) {
lower = self.upper;
upper = self.lower;
}
assert(lower <= upper);
return lower <= number and number <= upper;
}
pub fn remapTo(from: Self, to: Self, value: Number) Number {
return remap_number(Number, from.lower, from.upper, to.lower, to.upper, value);
}
pub fn add(self: Self, amount: Number) Self {
return init(
self.lower + amount,
self.upper + amount
);
}
pub fn sub(self: Self, amount: Number) Self {
return init(
self.lower - amount,
self.upper - amount
);
}
pub fn mul(self: Self, factor: Number) Self {
return init(
self.lower * factor,
self.upper * factor
);
}
pub fn zoom(self: Self, center: Number, factor: Number) Self {
return init(
(self.lower - center) * factor + center,
(self.upper - center) * factor + center
);
}
pub fn intersectPositive(self: Self, other: Self) Self {
// TODO: Figure out how would an intersection of "negative" ranges should look
// For now just coerce the negative ranges to positive ones.
const self_positive = self.toPositive();
const other_positive = other.toPositive();
return init(
@max(self_positive.lower, other_positive.lower),
@min(self_positive.upper, other_positive.upper)
);
}
pub fn toPositive(self: Self) Self {
if (self.isPositive()) {
return self;
} else {
return self.flip();
}
}
pub fn isPositive(self: Self) bool {
return self.upper >= self.lower;
}
pub fn isNegative(self: Self) bool {
return self.lower >= self.upper;
}
pub fn grow(self: Self, amount: Number) Self {
if (self.isPositive()) {
return init(
self.lower - amount,
self.upper + amount
);
} else {
return init(
self.lower + amount,
self.upper - amount
);
}
}
};
}
pub const RangeF32 = Range(f32);
pub const RangeF64 = Range(f64);
test "math operations" {
try std.testing.expectEqual(
RangeF32.init(0, 10).mul(5),
RangeF32.init(0, 50)
);
try std.testing.expectEqual(
RangeF32.init(0, 10).add(5),
RangeF32.init(5, 15)
);
try std.testing.expectEqual(
RangeF32.init(0, 10).sub(5),
RangeF32.init(-5, 5)
);
}
test "size" {
try std.testing.expectEqual(
RangeF32.init(0, 10).size(),
10
);
try std.testing.expectEqual(
RangeF32.init(-10, 0).size(),
10
);
}
test "intersection" {
try std.testing.expectEqual(
RangeF32.init(0, 10).intersectPositive(RangeF32.init(5, 8)),
RangeF32.init(5, 8)
);
try std.testing.expectEqual(
RangeF32.init(0, 10).intersectPositive(RangeF32.init(-5, 8)),
RangeF32.init(0, 8)
);
try std.testing.expectEqual(
RangeF32.init(0, 10).intersectPositive(RangeF32.init(5, 15)),
RangeF32.init(5, 10)
);
try std.testing.expectEqual(
RangeF32.init(0, 10).intersectPositive(RangeF32.init(20, 30)),
RangeF32.init(20, 10)
);
}

View File

@ -10,6 +10,13 @@ pub fn position(rect: rl.Rectangle) rl.Vector2 {
return rl.Vector2.init(rect.x, rect.y);
}
pub fn positionAt(rect: rl.Rectangle, normalised_position: rl.Vector2) rl.Vector2 {
return rl.Vector2.init(
rect.x + normalised_position.x * rect.width,
rect.y + normalised_position.y * rect.height
);
}
pub fn size(rect: rl.Rectangle) rl.Vector2 {
return rl.Vector2.init(rect.width, rect.height);
}

View File

@ -0,0 +1,234 @@
const std = @import("std");
const App = @import("../app.zig");
const UI = @import("../ui.zig");
const srcery = @import("../srcery.zig");
const NIDaq = @import("../ni-daq/root.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.channel_from_device_screen);
const Screen = @This();
app: *App,
hot_channel: ?[:0]const u8 = null,
// TODO: 32 limit
selected_channels: std.BoundedArray([:0]u8, 32) = .{},
// TODO: Don't use arena
channel_names: std.heap.ArenaAllocator,
pub fn init(app: *App) Screen {
return Screen{
.app = app,
.channel_names = std.heap.ArenaAllocator.init(app.allocator)
};
}
pub fn deinit(self: *Screen) void {
_ = self.channel_names.reset(.free_all);
}
fn isChannelSelected(self: *Screen, channel: []const u8) bool {
for (self.selected_channels.slice()) |selected_channel| {
if (std.mem.eql(u8, selected_channel, channel)) {
return true;
}
}
return false;
}
fn selectChannel(self: *Screen, channel: []const u8) void {
if (self.selected_channels.unusedCapacitySlice().len == 0) {
log.warn("Maximum number of selected channels reached", .{});
return;
}
if (self.isChannelSelected(channel)) {
return;
}
const allocator = self.channel_names.allocator();
const channel_dupe = allocator.dupeZ(u8, channel) catch |e| {
log.err("Failed to duplicate channel name: {}", .{e});
return;
};
self.selected_channels.appendAssumeCapacity(channel_dupe);
}
fn deselectChannel(self: *Screen, channel: []const u8) void {
for (0.., self.selected_channels.slice()) |i, selected_channel| {
if (std.mem.eql(u8, selected_channel, channel)) {
_ = self.selected_channels.swapRemove(i);
return;
}
}
}
fn toggleChannel(self: *Screen, channel: []const u8) void {
if (self.isChannelSelected(channel)) {
self.deselectChannel(channel);
} else {
self.selectChannel(channel);
}
}
pub fn tick(self: *Screen) !void {
var ni_daq = self.app.ni_daq orelse return;
var ui = &self.app.ui;
if (ui.isKeyboardPressed(.key_escape)) {
self.app.screen = .main;
}
const root = ui.parentBox().?;
root.layout_direction = .left_to_right;
{
const panel = ui.beginScrollbar(ui.keyFromString("Channels"));
defer ui.endScrollbar();
panel.layout_direction = .top_to_bottom;
const devices = try ni_daq.listDeviceNames();
for (devices) |device| {
var ai_voltage_physical_channels: []const [:0]const u8 = &.{};
if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
ai_voltage_physical_channels = try ni_daq.listDeviceAIPhysicalChannels(device);
}
var ao_physical_channels: []const [:0]const u8 = &.{};
if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
ao_physical_channels = try ni_daq.listDeviceAOPhysicalChannels(device);
}
inline for (.{ ai_voltage_physical_channels, ao_physical_channels }) |channels| {
for (channels) |channel| {
const channel_button = ui.textButton(channel);
channel_button.background = srcery.black;
if (self.isChannelSelected(channel)) {
channel_button.background = srcery.bright_white;
channel_button.text_color = srcery.black;
}
if (self.app.getChannelByName(channel) != null) {
channel_button.text_color = srcery.white;
channel_button.background = srcery.hard_black;
} else {
const signal = ui.signal(channel_button);
if (signal.clicked()) {
self.toggleChannel(channel);
}
if (signal.hot) {
self.hot_channel = channel;
}
}
}
}
}
}
{
const panel = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
.borders = .{
.left = .{ .color = srcery.hard_black, .size = 4 }
},
.layout_direction = .top_to_bottom,
.padding = UI.Padding.all(ui.rem(2))
});
panel.beginChildren();
defer panel.endChildren();
const info_container = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
.layout_direction = .top_to_bottom,
});
if (self.hot_channel) |hot_channel| {
info_container.beginChildren();
defer info_container.endChildren();
var maybe_hot_device: ?[:0]const u8 = null;
var device_buff: NIDaq.BoundedDeviceName = .{};
if (NIDaq.getDeviceNameFromChannel(hot_channel)) |device| {
device_buff.appendSliceAssumeCapacity(device);
device_buff.buffer[device_buff.len] = 0;
maybe_hot_device = device_buff.buffer[0..device_buff.len :0];
}
var channel_type_name: []const u8 = "unknown";
if (NIDaq.getChannelType(hot_channel)) |channel_type| {
channel_type_name = channel_type.name();
}
{
const channel_info = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFitChildren(),
.padding = .{
.bottom = ui.rem(2)
},
.layout_direction = .top_to_bottom
});
channel_info.beginChildren();
defer channel_info.endChildren();
_ = ui.label("Channel properties", .{});
_ = ui.label("Name: {s}", .{hot_channel});
_ = ui.label("Type: {s}", .{channel_type_name});
}
if (maybe_hot_device) |hot_device| {
_ = ui.label("Device properties", .{});
if (ni_daq.listDeviceAIMeasurementTypes(hot_device)) |measurement_types| {
_ = ui.label("Measurement types: {} types", .{measurement_types.len});
} else |e| {
log.err("ni_daq.listDeviceAIMeasurementTypes(): {}", .{ e });
}
}
} else {
info_container.alignment.x = .center;
info_container.alignment.y = .center;
info_container.setText("Hover on a channel");
info_container.flags.insert(.wrap_text);
info_container.text_color = srcery.hard_black;
info_container.font = .{
.variant = .bold_italic,
.size = ui.rem(3)
};
}
const add_button = ui.button(ui.keyFromString("Add channels"));
add_button.setFmtText("Add {} selected channels", .{self.selected_channels.len});
add_button.size.x = UI.Sizing.initGrowFull();
add_button.alignment.x = .center;
if (self.selected_channels.len > 0) {
add_button.borders = UI.Borders.all(.{
.color = srcery.green,
.size = 2
});
const signal = ui.signal(add_button);
if (signal.clicked()) {
self.app.screen = .main;
for (self.selected_channels.slice()) |channel| {
_ = try self.app.addView(.{
.channel = try self.app.addChannel(channel)
});
}
}
} else {
add_button.borders = UI.Borders.all(.{
.color = srcery.hard_black,
.size = 2
});
}
}
}

533
src/screens/main_screen.zig Normal file
View File

@ -0,0 +1,533 @@
const std = @import("std");
const rl = @import("raylib");
const UI = @import("../ui.zig");
const App = @import("../app.zig");
const srcery = @import("../srcery.zig");
const Platform = @import("../platform.zig");
const RangeF64 = @import("../range.zig").RangeF64;
const Graph = @import("../graph.zig");
const Assets = @import("../assets.zig");
const utils = @import("../utils.zig");
const NIDaq = @import("../ni-daq/root.zig");
const UIView = @import("../components/view.zig");
const ViewControlsSystem = @import("../components/systems/view_controls.zig");
const MainScreen = @This();
const log = std.log.scoped(.main_screen);
const assert = std.debug.assert;
const remap = utils.remap;
const Id = App.Id;
app: *App,
view_controls: ViewControlsSystem,
// Protocol modal
frequency_input: UI.TextInputStorage,
amplitude_input: UI.TextInputStorage,
protocol_error_message: ?[]const u8 = null,
protocol_graph_cache: Graph.RenderCache = .{},
preview_samples: std.ArrayListUnmanaged(f64) = .{},
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
// Project settings
sample_rate_input: UI.TextInputStorage,
parsed_sample_rate: ?f64 = null,
pub fn init(app: *App) !MainScreen {
const allocator = app.allocator;
var self = MainScreen{
.app = app,
.frequency_input = UI.TextInputStorage.init(allocator),
.amplitude_input = UI.TextInputStorage.init(allocator),
.sample_rate_input = UI.TextInputStorage.init(allocator),
.view_controls = ViewControlsSystem.init(&app.project)
};
try self.frequency_input.setText("10");
try self.amplitude_input.setText("10");
return self;
}
pub fn deinit(self: *MainScreen) void {
const allocator = self.app.allocator;
self.frequency_input.deinit();
self.amplitude_input.deinit();
self.sample_rate_input.deinit();
self.preview_samples.clearAndFree(allocator);
self.clearProtocolErrorMessage();
}
pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
var ui = &self.app.ui;
const allocator = self.app.allocator;
const view = self.app.getView(view_id) orelse return;
if (view.reference != .channel) return;
const channel_id = view.reference.channel;
const channel = self.app.getChannel(channel_id) orelse return;
const sample_rate = self.app.project.getSampleRate() orelse return;
const container = ui.createBox(.{
.key = ui.keyFromString("Protocol modal"),
.background = srcery.black,
.size_x = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }),
.size_y = UI.Sizing.initGrowUpTo(.{ .pixels = 300 }),
.layout_direction = .top_to_bottom,
.padding = UI.Padding.all(ui.rem(1.5)),
.flags = &.{ .clickable },
.layout_gap = ui.rem(0.5)
});
container.beginChildren();
defer container.endChildren();
{
const protocol_view = ui.createBox(.{
.key = ui.keyFromString("Protocol view"),
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedPixels(ui.rem(4)),
.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 })
});
const samples = self.preview_samples.items;
const view_rect = Graph.ViewOptions{
.x_range = RangeF64.init(0, @floatFromInt(samples.len)),
.y_range = self.preview_samples_y_range
};
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, samples);
if (self.protocol_graph_cache.texture) |texture| {
protocol_view.texture = texture.texture;
}
}
const FormInput = struct {
name: []const u8,
storage: *UI.TextInputStorage,
value: *f32
};
var frequency: f32 = 0;
var amplitude: f32 = 0;
const form_inputs = &[_]FormInput{
.{
.name = "Frequency",
.storage = &self.frequency_input,
.value = &frequency
},
.{
.name = "Amplitude",
.storage = &self.amplitude_input,
.value = &amplitude
},
};
var any_input_modified = false;
for (form_inputs) |form_input| {
const label = form_input.name;
const text_input_storage = form_input.storage;
const row = ui.createBox(.{
.key = ui.keyFromString(label),
.layout_direction = .left_to_right,
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedPixels(ui.rem(1.5))
});
row.beginChildren();
defer row.endChildren();
const label_box = ui.label("{s}", .{ label });
label_box.size.x = UI.Sizing.initFixed(.{ .font_size = 5 });
label_box.size.y = UI.Sizing.initGrowFull();
label_box.alignment.y = .center;
try ui.textInput(.{
.key = ui.keyFromString("Text input"),
.storage = text_input_storage
});
any_input_modified = any_input_modified or text_input_storage.modified;
}
if (any_input_modified) {
self.clearProtocolErrorMessage();
for (form_inputs) |form_input| {
const label = form_input.name;
const text_input_storage: *UI.TextInputStorage = form_input.storage;
const number = std.fmt.parseFloat(f32, text_input_storage.buffer.items) catch {
try self.setProtocolErrorMessage("ERROR: {s} must be a number", .{ label });
continue;
};
if (number <= 0) {
try self.setProtocolErrorMessage("ERROR: {s} must be positive", .{ label });
continue;
}
form_input.value.* = number;
}
}
if (self.protocol_error_message == null and any_input_modified) {
try App.Channel.generateSine(&self.preview_samples, allocator, sample_rate, frequency, amplitude);
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
self.protocol_graph_cache.invalidate();
}
if (self.protocol_error_message) |message| {
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedText(),
.text_color = srcery.red,
.align_x = .start,
.align_y = .start,
.flags = &.{ .wrap_text },
.text = message
});
}
_ = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
});
{
const row = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFitChildren(),
.align_x = .end
});
row.beginChildren();
defer row.endChildren();
const btn = ui.textButton("Confirm");
btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 });
if (ui.signal(btn).clicked() or ui.isKeyboardPressed(.key_enter)) {
if (self.protocol_error_message == null) {
try App.Channel.generateSine(&channel.write_pattern, allocator, sample_rate, frequency, amplitude);
self.app.pushCommand(.{ .start_output = channel_id });
self.view_controls.view_protocol_modal = null;
}
}
}
_ = ui.signal(container);
}
fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void {
self.clearProtocolErrorMessage();
const allocator = self.app.allocator;
self.protocol_error_message = try std.fmt.allocPrint(allocator, fmt, args);
}
fn clearProtocolErrorMessage(self: *MainScreen) void {
const allocator = self.app.allocator;
if (self.protocol_error_message) |msg| {
allocator.free(msg);
}
self.protocol_error_message = null;
}
pub fn showSidePanel(self: *MainScreen) !void {
var ui = &self.app.ui;
const frame_allocator = ui.frameAllocator();
const container = ui.createBox(.{
.size_x = UI.Sizing.initGrowUpTo(.{ .parent_percent = 0.2 }),
.size_y = UI.Sizing.initGrowFull(),
.borders = .{
.right = .{ .color = srcery.hard_black, .size = 4 }
},
.layout_direction = .top_to_bottom,
.padding = UI.Padding.all(ui.rem(1)),
.layout_gap = ui.rem(0.2)
});
container.beginChildren();
defer container.endChildren();
const project = &self.app.project;
const sample_rate = project.getSampleRate();
if (self.view_controls.view_settings) |view_id| {
const view = project.views.get(view_id) orelse return;
{
const label = ui.label("Settings", .{});
label.borders.bottom = .{
.color = srcery.bright_white,
.size = 1
};
}
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
_ = ui.checkbox(.{
.value = &view.sync_controls,
.label = "Sync controls"
});
var sample_count: ?usize = null;
switch (view.reference) {
.channel => |channel_id| {
const channel = project.channels.get(channel_id).?;
const channel_name = utils.getBoundedStringZ(&channel.name);
const channel_type = NIDaq.getChannelType(channel_name);
const samples = channel.collected_samples.items;
_ = ui.label("Channel: {s}", .{ channel_name });
if (channel_type != null) {
_ = ui.label("Type: {s}", .{ channel_type.?.name() });
} else {
_ = ui.label("Type: unknown", .{ });
}
sample_count = samples.len;
},
.file => |file_id| {
const file = project.files.get(file_id).?;
if (file.samples) |samples| {
sample_count = samples.len;
}
}
}
if (sample_count != null) {
_ = ui.label("Samples: {d}", .{ sample_count.? });
var duration_str: []const u8 = "-";
if (sample_rate != null) {
const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?;
if (utils.formatDuration(ui.frameAllocator(), duration)) |str| {
duration_str = str;
} else |_| {}
}
_ = ui.label("Duration: {s}", .{ duration_str });
}
} else {
{
const label = ui.label("Project", .{});
label.borders.bottom = .{
.color = srcery.bright_white,
.size = 1
};
}
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
{ // Sample rate
var placeholder: ?[]const u8 = null;
if (project.getDefaultSampleRate()) |default_sample_rate| {
placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate });
}
var initial: ?[]const u8 = null;
if (project.sample_rate) |selected_sample_rate| {
initial = try std.fmt.allocPrint(frame_allocator, "{d}", .{ selected_sample_rate });
}
_ = ui.label("Sample rate", .{});
self.parsed_sample_rate = try ui.numberInput(f64, .{
.key = ui.keyFromString("Sample rate input"),
.storage = &self.sample_rate_input,
.placeholder = placeholder,
.initial = initial,
.invalid = self.parsed_sample_rate != project.sample_rate,
.editable = !self.app.isCollectionInProgress()
});
project.sample_rate = self.parsed_sample_rate;
if (project.getAllowedSampleRates()) |allowed_sample_rates| {
if (project.sample_rate) |selected_sample_rate| {
if (!allowed_sample_rates.hasInclusive(selected_sample_rate)) {
project.sample_rate = null;
}
}
}
}
_ = ui.checkbox(.{
.value = &project.show_rulers,
.label = "Ruler"
});
}
}
pub fn tick(self: *MainScreen) !void {
var ui = &self.app.ui;
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_z)) {
self.view_controls.undoLastMove();
}
const root = ui.parentBox().?;
root.layout_direction = .top_to_bottom;
const was_protocol_modal_open = self.view_controls.view_protocol_modal != null;
var maybe_modal_overlay: ?*UI.Box = null;
if (self.view_controls.view_protocol_modal) |view_id| {
const padding = UI.Padding.all(ui.rem(2));
const modal_overlay = ui.createBox(.{
.key = ui.keyFromString("Overlay"),
.float_rect = .{
.x = 0,
.y = 0,
// TODO: This is a hack, UI core should handle this
.width = root.persistent.size.x - padding.byAxis(.X),
.height = root.persistent.size.y - padding.byAxis(.Y)
},
.background = rl.Color.black.alpha(0.6),
.flags = &.{ .clickable, .scrollable },
.padding = padding,
.align_x = .center,
.align_y = .center,
});
modal_overlay.beginChildren();
defer modal_overlay.endChildren();
try self.showProtocolModal(view_id);
if (ui.signal(modal_overlay).clicked()) {
self.view_controls.view_protocol_modal = null;
}
maybe_modal_overlay = modal_overlay;
}
{
const toolbar = ui.createBox(.{
.background = srcery.black,
.layout_direction = .left_to_right,
.size_x = .{ .fixed = .{ .parent_percent = 1 } },
.size_y = .{ .fixed = .{ .font_size = 2 } },
.borders = .{
.bottom = .{ .color = srcery.hard_black, .size = 4 }
}
});
toolbar.beginChildren();
defer toolbar.endChildren();
{
var btn = ui.textButton("Start/Stop button");
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.red });
btn.background = srcery.black;
btn.size.y = UI.Sizing.initFixed(.{ .parent_percent = 1 });
btn.padding.top = 0;
btn.padding.bottom = 0;
if (ui.signal(btn).clicked()) {
if (self.app.isCollectionInProgress()) {
self.app.pushCommand(.stop_collection);
} else {
self.app.pushCommand(.start_collection);
}
}
if (self.app.isCollectionInProgress()) {
btn.setText("Stop");
} else {
btn.setText("Start");
}
}
{
var btn = ui.textButton("Save");
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green });
if (ui.signal(btn).clicked()) {
self.app.pushCommand(.save_project);
}
}
}
const ui_view_ctx = UIView.Context{
.app = self.app,
.ui = &self.app.ui,
.view_controls = &self.view_controls
};
if (self.view_controls.view_fullscreen) |view_id| {
_ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initGrowFull());
} else {
const container = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initGrowFull(),
.layout_direction = .left_to_right
});
container.beginChildren();
defer container.endChildren();
try self.showSidePanel();
const scroll_area = ui.beginScrollbar(ui.keyFromString("Channels"));
defer ui.endScrollbar();
scroll_area.layout_direction = .top_to_bottom;
var view_iter = self.app.project.views.idIterator();
while (view_iter.next()) |view_id| {
const view = self.app.getView(view_id);
_ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initFixed(.{ .pixels = view.?.height }));
}
{
const add_channel_view = ui.createBox(.{
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixed(.{ .pixels = 200 }),
.align_x = .center,
.align_y = .center,
.layout_gap = 32
});
add_channel_view.beginChildren();
defer add_channel_view.endChildren();
const add_from_file = ui.textButton("Add from file");
add_from_file.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green });
if (ui.signal(add_from_file).clicked()) {
self.app.pushCommand(.add_file_from_picker);
}
const add_from_device = ui.textButton("Add from device");
add_from_device.borders = UI.Borders.all(.{ .size = 2, .color = srcery.green });
if (ui.signal(add_from_device).clicked()) {
self.app.screen = .add_channels;
}
}
}
self.view_controls.applyCommands();
if (maybe_modal_overlay) |modal_overlay| {
root.bringChildToTop(modal_overlay);
}
if (ui.isKeyboardPressed(.key_escape)) {
if (self.view_controls.view_protocol_modal != null) {
self.view_controls.view_protocol_modal = null;
} else if (self.view_controls.view_fullscreen != null) {
self.view_controls.view_fullscreen = null;
} else if (self.view_controls.view_settings != null) {
self.view_controls.view_settings = null;
} else {
self.app.should_close = true;
}
}
const is_protocol_modal_open = self.view_controls.view_protocol_modal != null;
if (!was_protocol_modal_open and is_protocol_modal_open) {
self.protocol_graph_cache.clear();
}
}

3349
src/ui.zig

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
const std = @import("std");
const rl = @import("raylib");
const assert = std.debug.assert;
pub fn vec2Round(vec2: rl.Vector2) rl.Vector2 {
return rl.Vector2{
.x = @round(vec2.x),
@ -16,6 +18,30 @@ pub fn rgba(r: u8, g: u8, b: u8, a: f32) rl.Color {
return rl.Color.init(r, g, b, a * 255);
}
pub fn lerpColor(from: rl.Color, to: rl.Color, t: f32) rl.Color {
const r = std.math.lerp(@as(f32, @floatFromInt(from.r)), @as(f32, @floatFromInt(to.r)), t);
const g = std.math.lerp(@as(f32, @floatFromInt(from.g)), @as(f32, @floatFromInt(to.g)), t);
const b = std.math.lerp(@as(f32, @floatFromInt(from.b)), @as(f32, @floatFromInt(to.b)), t);
const a = std.math.lerp(@as(f32, @floatFromInt(from.a)), @as(f32, @floatFromInt(to.a)), t);
return rl.Color{
.r = @intFromFloat(r),
.g = @intFromFloat(g),
.b = @intFromFloat(b),
.a = @intFromFloat(a),
};
}
pub fn shiftColorInHSV(color: rl.Color, value_shift: f32) rl.Color {
if (value_shift == 0) {
return color;
}
var hsv = rl.colorToHSV(color);
hsv.z = std.math.clamp(hsv.z * (1 + value_shift), 0, 1);
return rl.colorFromHSV(hsv.x, hsv.y, hsv.z);
}
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
rl.drawRectangleRec(rl.Rectangle{
.x = rect.x,
@ -29,3 +55,37 @@ pub fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, v
const t = (value - from_min) / (from_max - from_min);
return std.math.lerp(to_min, to_max, t);
}
pub fn roundNearestUp(comptime T: type, value: T, multiple: T) T {
return @ceil(value / multiple) * multiple;
}
pub fn roundNearestDown(comptime T: type, value: T, multiple: T) T {
return @floor(value / multiple) * multiple;
}
pub fn roundNearestTowardZero(comptime T: type, value: T, multiple: T) T {
return @trunc(value / multiple) * multiple;
}
pub fn initBoundedStringZ(comptime BoundedString: type, text: []const u8) !BoundedString {
var bounded_string = try BoundedString.fromSlice(text);
try bounded_string.append(0);
return bounded_string;
}
pub fn getBoundedStringZ(bounded_array: anytype) [:0]const u8 {
return bounded_array.buffer[0..(bounded_array.len-1) :0];
}
pub inline fn dumpErrorTrace() void {
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
}
pub fn formatDuration(allocator: std.mem.Allocator, total_seconds: f64) ![]u8 {
const seconds = @mod(total_seconds, @as(f64, @floatFromInt(std.time.s_per_min)));
const minutes = @divFloor(total_seconds, std.time.s_per_min);
return try std.fmt.allocPrint(allocator, "{d:.0}m {d:.3}s", .{ minutes, seconds });
}

44
tools/generate.zig Normal file
View File

@ -0,0 +1,44 @@
const std = @import("std");
pub fn show_usage() void {
std.debug.print("Usage: zig build generate <sample-rate> <sample-count> <filename>\n", .{});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
_ = args.next();
const sample_rate_str = args.next() orelse {
show_usage();
std.process.exit(1);
};
const sample_count_str = args.next() orelse {
show_usage();
std.process.exit(1);
};
const filename = args.next() orelse {
show_usage();
std.process.exit(1);
};
const sample_rate = try std.fmt.parseFloat(f64, sample_rate_str);
const sample_count = try std.fmt.parseInt(u32, sample_count_str, 10);
const f = try std.fs.cwd().createFile(filename, .{ .exclusive = true });
defer f.close();
for (0..sample_count) |i| {
const i_f64: f64 = @floatFromInt(i);
const sample: f64 = std.math.sin( i_f64 / sample_rate * std.math.pi * 2 ) * 10;
const sample_bytes = std.mem.toBytes(sample);
try f.writeAll(&sample_bytes);
}
}