Compare commits
22 Commits
e3588f6836
...
a2fc2befd1
Author | SHA1 | Date | |
---|---|---|---|
a2fc2befd1 | |||
8d1cad16b3 | |||
772f35fee7 | |||
36706ff348 | |||
5c8a4ebb54 | |||
ae26d2e1ba | |||
23c4b99455 | |||
293d220b34 | |||
d2b4942fa0 | |||
c588956226 | |||
31d0af0a5c | |||
8ba3d0c914 | |||
25377c8b4f | |||
d8867cb3d6 | |||
de2941c5bf | |||
859c36e93a | |||
6e332df183 | |||
778c0f4cb9 | |||
877f8034c7 | |||
a6a66d99fd | |||
e33ab321d0 | |||
09fccb7069 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.zig-cache
|
||||
zig-out
|
||||
.vscode
|
||||
.vscode
|
||||
profile.json
|
||||
|
58
build.zig
58
build.zig
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 = .{
|
||||
|
2309
src/app.zig
2309
src/app.zig
File diff suppressed because it is too large
Load Diff
126
src/assets.zig
126
src/assets.zig
@ -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();
|
||||
|
BIN
src/assets/fonts/roboto/Roboto-Bold.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/roboto/Roboto-BoldItalic.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/roboto/Roboto-Italic.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/roboto/Roboto-Thin.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/roboto/Roboto-ThinItalic.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
src/assets/fullscreen-icon.ase
Normal file
BIN
src/assets/fullscreen-icon.ase
Normal file
Binary file not shown.
BIN
src/assets/output-generation-icon.ase
Normal file
BIN
src/assets/output-generation-icon.ase
Normal file
Binary file not shown.
286
src/components/systems/view_controls.zig
Normal file
286
src/components/systems/view_controls.zig
Normal 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
318
src/components/view.zig
Normal 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;
|
||||
}
|
335
src/components/view_ruler.zig
Normal file
335
src/components/view_ruler.zig
Normal 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
7
src/constants.zig
Normal 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;
|
@ -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 {
|
||||
|
408
src/graph.zig
408
src/graph.zig
@ -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);
|
||||
}
|
||||
}
|
160
src/main.zig
160
src/main.zig
@ -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");
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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
196
src/range.zig
Normal 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)
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
234
src/screens/channel_from_device.zig
Normal file
234
src/screens/channel_from_device.zig
Normal 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
533
src/screens/main_screen.zig
Normal 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 = &litude
|
||||
},
|
||||
};
|
||||
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
3349
src/ui.zig
File diff suppressed because it is too large
Load Diff
@ -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
44
tools/generate.zig
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user