refactor sample list to store in blocks
This commit is contained in:
parent
af8cf21f53
commit
e48887cdee
599
src/app.zig
599
src/app.zig
@ -167,19 +167,293 @@ fn GenerationalArray(Item: type) type {
|
||||
}
|
||||
|
||||
pub const SampleList = struct {
|
||||
samples: std.ArrayListUnmanaged(f64) = .{},
|
||||
graph_min_max_cache: Graph.MinMaxCache = .{},
|
||||
min_sample: ?f64 = null,
|
||||
max_sample: ?f64 = null,
|
||||
pub const Block = struct {
|
||||
pub const Id = usize;
|
||||
pub const capacity = 512;
|
||||
pub const Len = std.math.IntFittingRange(0, capacity);
|
||||
const Buffer = [capacity]f64;
|
||||
|
||||
pub fn clear(self: *SampleList, allocator: Allocator) void {
|
||||
self.deinit(allocator);
|
||||
self.* = .{};
|
||||
comptime {
|
||||
assert(std.math.isPowerOfTwo(capacity));
|
||||
}
|
||||
|
||||
buffer: *Buffer,
|
||||
len: Len = 0,
|
||||
|
||||
min: ?f64 = null,
|
||||
max: ?f64 = null,
|
||||
|
||||
pub fn samplesSlice(self: *Block) []f64 {
|
||||
return self.buffer[0..self.len];
|
||||
}
|
||||
|
||||
pub fn append(self: *Block, samples: []const f64) usize {
|
||||
const unused_slice = self.buffer[self.len..];
|
||||
|
||||
const copy_amount = @min(unused_slice.len, samples.len);
|
||||
const added_samples = samples[0..copy_amount];
|
||||
@memcpy(unused_slice[0..copy_amount], added_samples);
|
||||
|
||||
self.len += @intCast(copy_amount);
|
||||
assert(self.len <= capacity);
|
||||
|
||||
if (added_samples.len > 0) {
|
||||
var min = self.min orelse added_samples[0];
|
||||
var max = self.max orelse added_samples[0];
|
||||
for (added_samples) |sample| {
|
||||
min = @min(min, sample);
|
||||
max = @max(max, sample);
|
||||
}
|
||||
self.min = min;
|
||||
self.max = max;
|
||||
}
|
||||
|
||||
return copy_amount;
|
||||
}
|
||||
|
||||
pub fn clear(self: *Block) void {
|
||||
self.* = Block{
|
||||
.buffer = self.buffer
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Iterator = struct {
|
||||
sample_list: *SampleList,
|
||||
current: usize,
|
||||
to: usize,
|
||||
|
||||
pub fn next(self: *Iterator) ?[]f64 {
|
||||
while (self.current < self.to) {
|
||||
const block = self.sample_list.getBlockAtSample(self.current) orelse return null;
|
||||
const from_index_in_block = @mod(self.current, Block.capacity);
|
||||
const to_index_in_block = @min(self.to - self.current + from_index_in_block, block.len);
|
||||
self.current += Block.capacity - from_index_in_block;
|
||||
|
||||
if (from_index_in_block >= block.len) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const segment = block.buffer[from_index_in_block..to_index_in_block];
|
||||
if (segment.len == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const BlockIdIterator = struct {
|
||||
current: Block.Id,
|
||||
to: Block.Id,
|
||||
|
||||
pub fn next(self: *BlockIdIterator) ?Block.Id {
|
||||
if (self.current > self.to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
defer self.current += 1;
|
||||
return self.current;
|
||||
}
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
blocks: std.ArrayListUnmanaged(Block) = .{},
|
||||
|
||||
min: ?f64 = null,
|
||||
max: ?f64 = null,
|
||||
|
||||
pub fn init(allocator: Allocator) SampleList {
|
||||
return SampleList{
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SampleList, allocator: Allocator) void {
|
||||
self.samples.clearAndFree(allocator);
|
||||
self.graph_min_max_cache.deinit(allocator);
|
||||
pub fn deinit(self: *SampleList) void {
|
||||
const allocator = self.arena.child_allocator;
|
||||
|
||||
self.arena.deinit();
|
||||
self.blocks.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn clear(self: *SampleList, allocator: Allocator) void {
|
||||
self.deinit();
|
||||
self.* = SampleList.init(allocator);
|
||||
}
|
||||
|
||||
fn getBlockAtSample(self: *SampleList, sample_index: usize) ?*Block {
|
||||
return self.getBlock(@divFloor(sample_index, Block.capacity));
|
||||
}
|
||||
|
||||
pub fn getBlock(self: *SampleList, block_id: Block.Id) ?*Block {
|
||||
if (block_id < self.blocks.items.len) {
|
||||
return &self.blocks.items[block_id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn append(self: *SampleList, samples: []const f64) !void {
|
||||
if (samples.len == 0) return;
|
||||
|
||||
var appended_count: usize = 0;
|
||||
while (appended_count < samples.len) {
|
||||
if (self.blocks.items.len == 0 or self.blocks.getLast().len == Block.capacity) {
|
||||
const allocator = self.arena.child_allocator;
|
||||
const buffer_allocator = self.arena.allocator();
|
||||
|
||||
const block_buffer = try buffer_allocator.create(Block.Buffer);
|
||||
errdefer buffer_allocator.destroy(block_buffer);
|
||||
|
||||
try self.blocks.append(allocator, Block{
|
||||
.buffer = block_buffer
|
||||
});
|
||||
}
|
||||
|
||||
const last_block = &self.blocks.items[self.blocks.items.len - 1];
|
||||
appended_count += last_block.append(samples[appended_count..]);
|
||||
|
||||
if (last_block.min) |block_min| {
|
||||
self.min = @min(self.min orelse block_min, block_min);
|
||||
}
|
||||
|
||||
if (last_block.max) |block_max| {
|
||||
self.max = @max(self.max orelse block_max, block_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getLength(self: *const SampleList) usize {
|
||||
var length: usize = 0;
|
||||
if (self.blocks.items.len > 1) {
|
||||
length += (self.blocks.items.len - 1) * Block.capacity;
|
||||
}
|
||||
|
||||
if (self.blocks.items.len > 0) {
|
||||
length += self.blocks.getLast().len;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
pub fn iterator(self: *SampleList, from: usize, to: usize) Iterator {
|
||||
return Iterator{
|
||||
.sample_list = self,
|
||||
.current = from,
|
||||
.to = to
|
||||
};
|
||||
}
|
||||
|
||||
pub fn blockIdIterator(self: *SampleList, from_sample: usize, to_sample: usize) BlockIdIterator {
|
||||
_ = self;
|
||||
return BlockIdIterator{
|
||||
.current = @divFloor(from_sample, Block.capacity),
|
||||
.to = @divFloor(to_sample, Block.capacity),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeToFile(self: *SampleList, file: std.fs.File) !void {
|
||||
const writer = file.writer();
|
||||
var last_sample: f64 = 0;
|
||||
for (0.., self.blocks.items) |i, *block| {
|
||||
const block_samples = block.samplesSlice();
|
||||
if (block_samples.len > 0) {
|
||||
try writer.writeAll(std.mem.sliceAsBytes(block_samples));
|
||||
last_sample = block_samples[block_samples.len - 1];
|
||||
}
|
||||
|
||||
if (i < self.blocks.items.len - 1) {
|
||||
const unused_size = SampleList.Block.capacity - block_samples.len;
|
||||
if (unused_size > 0) {
|
||||
try writer.writeBytesNTimes(std.mem.asBytes(&last_sample), unused_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** 256);
|
||||
try std.testing.expect(sample_list.block_sizes.items[0] == 256);
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** 256);
|
||||
try std.testing.expect(sample_list.block_sizes.items[0] == 512);
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** 100);
|
||||
try std.testing.expect(sample_list.block_sizes.items[1] == 100);
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
|
||||
|
||||
try std.testing.expect(
|
||||
sample_list.getBlockAtSample(Block.capacity - 1).?.ptr == sample_list.getBlock(0).ptr
|
||||
);
|
||||
try std.testing.expect(
|
||||
sample_list.getBlockAtSample(Block.capacity).?.ptr == sample_list.getBlock(1).ptr
|
||||
);
|
||||
try std.testing.expect(
|
||||
sample_list.getBlockAtSample(2*Block.capacity).?.ptr == sample_list.getBlock(2).ptr
|
||||
);
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
|
||||
|
||||
var iter = sample_list.iterator(0, Block.capacity);
|
||||
|
||||
try std.testing.expectEqual(sample_list.getBlock(0), iter.next());
|
||||
try std.testing.expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
|
||||
|
||||
var iter = sample_list.iterator(256, Block.capacity);
|
||||
|
||||
try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next());
|
||||
try std.testing.expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
|
||||
|
||||
var iter = sample_list.iterator(256, Block.capacity + 256);
|
||||
|
||||
try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next());
|
||||
try std.testing.expectEqual(sample_list.getBlock(1)[0..256], iter.next());
|
||||
try std.testing.expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test {
|
||||
var sample_list = SampleList.init(std.testing.allocator);
|
||||
defer sample_list.deinit();
|
||||
|
||||
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
|
||||
|
||||
var iter = sample_list.iterator(400, 2*Block.capacity + 100);
|
||||
|
||||
try std.testing.expectEqual(sample_list.getBlock(0)[400..], iter.next());
|
||||
try std.testing.expectEqual(sample_list.getBlock(1), iter.next());
|
||||
try std.testing.expectEqual(sample_list.getBlock(2)[0..100], iter.next());
|
||||
try std.testing.expectEqual(null, iter.next());
|
||||
}
|
||||
};
|
||||
|
||||
@ -297,26 +571,32 @@ pub const View = struct {
|
||||
self.standard_deviation = null;
|
||||
}
|
||||
|
||||
pub fn refresh(self: *MarkedRange, samples: []const f64) void {
|
||||
pub fn refresh(self: *MarkedRange, sample_list: *SampleList) void {
|
||||
self.clear();
|
||||
|
||||
if (self.axis == .X) {
|
||||
const from = std.math.clamp(@as(usize, @intFromFloat(@floor(self.range.lower))), 0, samples.len);
|
||||
const to = std.math.clamp(@as(usize, @intFromFloat(@ceil(self.range.upper))), 0, samples.len);
|
||||
if (to - from == 0) return;
|
||||
var sample_count: f64 = 0;
|
||||
var sum: f64 = 0;
|
||||
var min = std.math.inf(f64);
|
||||
var max = -std.math.inf(f64);
|
||||
|
||||
const samples_in_range = samples[from..to];
|
||||
if (samples_in_range.len > 0) {
|
||||
const sample_count: f64 = @floatFromInt(samples_in_range.len);
|
||||
const from_sample: usize = @intFromFloat(@floor(std.math.clamp(self.range.lower, 0, @as(f64, @floatFromInt(sample_list.getLength())))));
|
||||
const to_sample: usize = @intFromFloat(@ceil(std.math.clamp(self.range.upper, 0, @as(f64, @floatFromInt(sample_list.getLength())))));
|
||||
|
||||
var sum: f64 = 0;
|
||||
var min = samples_in_range[0];
|
||||
var max = samples_in_range[0];
|
||||
for (samples_in_range) |sample| {
|
||||
min = @min(min, sample);
|
||||
max = @max(max, sample);
|
||||
sum += sample;
|
||||
{
|
||||
var iter = sample_list.iterator(from_sample, to_sample);
|
||||
while (iter.next()) |segment| {
|
||||
sample_count += @floatFromInt(segment.len);
|
||||
|
||||
for (segment) |sample| {
|
||||
min = @min(min, sample);
|
||||
max = @max(max, sample);
|
||||
sum += sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sample_count > 0) {
|
||||
const average = sum / sample_count;
|
||||
|
||||
self.min = min;
|
||||
@ -325,8 +605,11 @@ pub const View = struct {
|
||||
|
||||
if (sample_count > 1) {
|
||||
var standard_deviation: f64 = 0;
|
||||
for (samples_in_range) |sample| {
|
||||
standard_deviation += (sample - average) * (sample - average);
|
||||
var iter = sample_list.iterator(from_sample, to_sample);
|
||||
while (iter.next()) |segment| {
|
||||
for (segment) |sample| {
|
||||
standard_deviation += (sample - average) * (sample - average);
|
||||
}
|
||||
}
|
||||
standard_deviation /= (sample_count - 1);
|
||||
standard_deviation = @sqrt(standard_deviation);
|
||||
@ -379,7 +662,8 @@ pub const View = struct {
|
||||
available_x_range: RangeF64 = RangeF64.init(0, 0),
|
||||
available_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
unit: ?NIDaq.Unit = .Voltage,
|
||||
transformed_samples: ?[]f64 = null,
|
||||
|
||||
transformed_samples: ?Id = null,
|
||||
|
||||
pub fn clear(self: *View) void {
|
||||
self.graph_cache.clear();
|
||||
@ -401,19 +685,6 @@ pub const View = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn appendMarkedRange(self: *View, axis: UI.Axis, range: RangeF64) ?*MarkedRange {
|
||||
if (self.marked_ranges.unusedCapacitySlice().len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marked_range = self.marked_ranges.addOneAssumeCapacity();
|
||||
marked_range.* = MarkedRange{
|
||||
.axis = axis,
|
||||
.range = range
|
||||
};
|
||||
return marked_range;
|
||||
}
|
||||
|
||||
pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator {
|
||||
return MarkedRangeIterator{
|
||||
.view = self,
|
||||
@ -469,7 +740,7 @@ pub const Project = struct {
|
||||
return result_range;
|
||||
}
|
||||
|
||||
pub fn getViewSampleList(self: *Project, view_id: Id) Id {
|
||||
pub fn getViewSampleListId(self: *Project, view_id: Id) Id {
|
||||
const view = self.views.get(view_id).?;
|
||||
|
||||
switch (view.reference) {
|
||||
@ -484,75 +755,150 @@ pub const Project = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getViewSamples(self: *Project, view_id: Id) []const f64 {
|
||||
const samples_list_id = self.getViewSampleList(view_id);
|
||||
const samples_list = self.sample_lists.get(samples_list_id).?;
|
||||
|
||||
return samples_list.samples.items;
|
||||
}
|
||||
|
||||
pub fn appendSamples(self: *Project, allocator: Allocator, sample_list_id: Id, samples: []const f64) !void {
|
||||
if (samples.len == 0) return;
|
||||
|
||||
const sample_list = self.sample_lists.get(sample_list_id).?;
|
||||
|
||||
const affected_range = RangeF64.init(
|
||||
@floatFromInt(sample_list.samples.items.len),
|
||||
@floatFromInt(sample_list.samples.items.len + samples.len)
|
||||
@floatFromInt(sample_list.getLength()),
|
||||
@floatFromInt(sample_list.getLength() + samples.len)
|
||||
);
|
||||
|
||||
try sample_list.samples.appendSlice(allocator, samples);
|
||||
|
||||
var min_sample = sample_list.min_sample orelse samples[0];
|
||||
var max_sample = sample_list.max_sample orelse samples[0];
|
||||
for (samples) |sample| {
|
||||
min_sample = @min(min_sample, sample);
|
||||
max_sample = @max(max_sample, sample);
|
||||
}
|
||||
sample_list.min_sample = min_sample;
|
||||
sample_list.max_sample = max_sample;
|
||||
|
||||
try sample_list.graph_min_max_cache.updateLast(allocator, sample_list.samples.items);
|
||||
try sample_list.append(samples);
|
||||
|
||||
try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id);
|
||||
self.refreshMarkedRanges(sample_list_id, affected_range);
|
||||
}
|
||||
|
||||
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) void {
|
||||
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
|
||||
const sample_list = self.sample_lists.get(sample_list_id).?;
|
||||
|
||||
const affected_range = RangeF64.init(
|
||||
0,
|
||||
@floatFromInt(sample_list.samples.items.len)
|
||||
@floatFromInt(sample_list.getLength())
|
||||
);
|
||||
|
||||
sample_list.clear(allocator);
|
||||
|
||||
try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id);
|
||||
self.refreshMarkedRanges(sample_list_id, affected_range);
|
||||
}
|
||||
|
||||
pub fn addSampleList(self: *Project, allocator: Allocator) !Id {
|
||||
return try self.sample_lists.insert(SampleList.init(allocator));
|
||||
}
|
||||
|
||||
pub fn removeSampleList(self: *Project, sample_list_id: Id) void {
|
||||
const sample_list = self.sample_lists.get(sample_list_id) orelse return;
|
||||
sample_list.deinit();
|
||||
self.sample_lists.remove(sample_list_id);
|
||||
}
|
||||
|
||||
fn refreshMarkedRanges(self: *Project, sample_list_id: Id, affected_range: RangeF64) void {
|
||||
const sample_list = self.sample_lists.get(sample_list_id).?;
|
||||
const total_samples = sample_list.samples.items;
|
||||
_ = sample_list;
|
||||
_ = affected_range;
|
||||
|
||||
// const total_samples = sample_list.samples.items;
|
||||
|
||||
// var view_iter = self.views.idIterator();
|
||||
// while (view_iter.next()) |view_id| {
|
||||
// const view_sample_list_id = self.getViewSampleList(view_id);
|
||||
// if (!view_sample_list_id.eql(sample_list_id)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// const view = self.views.get(view_id).?;
|
||||
// const marked_ranges: []View.MarkedRange = view.marked_ranges.slice();
|
||||
// for (marked_ranges) |*marked_range| {
|
||||
// if (marked_range.axis != .X) continue;
|
||||
|
||||
// if (affected_range.intersectPositive(marked_range.range).isPositive()) {
|
||||
// marked_range.refresh(total_samples);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn refreshTransformedSamplesBySampleList(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
|
||||
var view_iter = self.views.idIterator();
|
||||
while (view_iter.next()) |view_id| {
|
||||
const view_sample_list_id = self.getViewSampleList(view_id);
|
||||
if (!view_sample_list_id.eql(sample_list_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const view = self.views.get(view_id).?;
|
||||
const marked_ranges: []View.MarkedRange = view.marked_ranges.slice();
|
||||
for (marked_ranges) |*marked_range| {
|
||||
if (marked_range.axis != .X) continue;
|
||||
|
||||
if (affected_range.intersectPositive(marked_range.range).isPositive()) {
|
||||
marked_range.refresh(total_samples);
|
||||
}
|
||||
const view_sample_list_id = self.getViewSampleListId(view_id);
|
||||
if (view_sample_list_id.eql(sample_list_id)) {
|
||||
try self.refreshTransformedSamplesByView(allocator, view_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refreshTransformedSamplesByView(self: *Project, allocator: Allocator, view_id: Id) !void {
|
||||
const view = self.views.get(view_id).?;
|
||||
|
||||
if (view.sliding_window) |sliding_window| {
|
||||
if (view.transformed_samples == null) {
|
||||
view.transformed_samples = try self.addSampleList(allocator);
|
||||
}
|
||||
// var transformed_sample_list = self.sample_lists.get(view.transformed_samples.?).?;
|
||||
|
||||
// const samples = self.getViewSamples(view_id);
|
||||
|
||||
// try transformed_sample_list.append(samples);
|
||||
|
||||
_ = sliding_window;
|
||||
} else {
|
||||
if (view.transformed_samples) |sample_list_id| {
|
||||
self.removeSampleList(sample_list_id);
|
||||
view.transformed_samples = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn readSamplesFromFile(self: *Project, allocator: Allocator, sample_list_id: Id, file: std.fs.File, sample_count: usize) !void {
|
||||
var bytes_left: usize = sample_count * 8;
|
||||
var buffer: [SampleList.Block.capacity * 8]u8 = undefined;
|
||||
while (true) {
|
||||
const count = try file.readAll(buffer[0..@min(bytes_left, buffer.len)]);
|
||||
if (count == 0) break;
|
||||
assert(count % 8 == 0);
|
||||
bytes_left -= count;
|
||||
|
||||
const samples = std.mem.bytesAsSlice(f64, buffer[0..count]);
|
||||
try self.appendSamples(allocator, sample_list_id, @alignCast(samples));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn updateSlidingWindow(self: *Project, view_id: Id, allocator: Allocator, sliding_window: ?f64) !void {
|
||||
const view = self.views.get(view_id).?;
|
||||
if (view.sliding_window == sliding_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.sliding_window = sliding_window;
|
||||
|
||||
try self.refreshTransformedSamplesByView(allocator, view_id);
|
||||
}
|
||||
|
||||
pub fn appendMarkedRange(self: *Project, view_id: Id, axis: UI.Axis, range: RangeF64) ?*View.MarkedRange {
|
||||
const view = self.views.get(view_id) orelse return null;
|
||||
|
||||
if (view.marked_ranges.unusedCapacitySlice().len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marked_range = view.marked_ranges.addOneAssumeCapacity();
|
||||
marked_range.* = View.MarkedRange{
|
||||
.axis = axis,
|
||||
.range = range
|
||||
};
|
||||
|
||||
const sample_list_id = self.getViewSampleListId(view_id);
|
||||
if (self.sample_lists.get(sample_list_id)) |sample_list| {
|
||||
marked_range.refresh(sample_list);
|
||||
}
|
||||
|
||||
return marked_range;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Project, allocator: Allocator) void {
|
||||
var file_iter = self.files.iterator();
|
||||
while (file_iter.next()) |file| {
|
||||
@ -621,8 +967,8 @@ pub const Project = struct {
|
||||
}
|
||||
errdefer if (saved_collected_samples) |str| allocator.free(str);
|
||||
|
||||
const sample_list_id = try self.sample_lists.insert(.{});
|
||||
errdefer self.sample_lists.remove(sample_list_id);
|
||||
const sample_list_id = try self.addSampleList(allocator);
|
||||
errdefer self.removeSampleList(sample_list_id);
|
||||
|
||||
const channel = self.channels.get(id).?;
|
||||
channel.* = Channel{
|
||||
@ -643,8 +989,8 @@ pub const Project = struct {
|
||||
const path = try readString(reader, allocator);
|
||||
errdefer allocator.free(path);
|
||||
|
||||
const sample_list_id = try self.sample_lists.insert(.{});
|
||||
errdefer self.sample_lists.remove(sample_list_id);
|
||||
const sample_list_id = try self.addSampleList(allocator);
|
||||
errdefer self.removeSampleList(sample_list_id);
|
||||
|
||||
const file = self.files.get(id).?;
|
||||
file.* = File{
|
||||
@ -735,8 +1081,8 @@ pub const Project = struct {
|
||||
const samples_file = try dir.createFile(path, .{});
|
||||
defer samples_file.close();
|
||||
|
||||
const samples = self.sample_lists.get(channel.collected_samples_id).?.samples.items;
|
||||
try writeFileF64(samples_file, samples);
|
||||
const sample_list = self.sample_lists.get(channel.collected_samples_id).?;
|
||||
try sample_list.writeToFile(samples_file);
|
||||
}
|
||||
|
||||
const stat = try dir.statFile(path);
|
||||
@ -838,6 +1184,7 @@ pub const Project = struct {
|
||||
|
||||
fn readString(reader: anytype, allocator: Allocator) ![]u8 {
|
||||
// TODO: This could be risky. `str_len` can be a really large number and in turn request a really large allocation.
|
||||
// Maybe some sane maximum should be implemented?
|
||||
const str_len = try readInt(reader, u32);
|
||||
const buff = try allocator.alloc(u8, str_len);
|
||||
errdefer allocator.free(buff);
|
||||
@ -1245,7 +1592,7 @@ fn startCollection(self: *App) !void {
|
||||
|
||||
for (channels_in_task.constSlice()) |id| {
|
||||
const channel = self.getChannel(id).?;
|
||||
self.project.clearSamples(self.allocator, channel.collected_samples_id);
|
||||
try self.project.clearSamples(self.allocator, channel.collected_samples_id);
|
||||
}
|
||||
|
||||
const read_array = try self.allocator.alloc(f64,
|
||||
@ -1420,8 +1767,8 @@ pub fn addFile(self: *App, path: []const u8) !Id {
|
||||
const path_dupe = try self.allocator.dupe(u8, path);
|
||||
errdefer self.allocator.free(path_dupe);
|
||||
|
||||
const sample_list_id = try self.project.sample_lists.insert(.{});
|
||||
errdefer self.project.sample_lists.remove(sample_list_id);
|
||||
const sample_list_id = try self.project.addSampleList(self.allocator);
|
||||
errdefer self.project.removeSampleList(sample_list_id);
|
||||
|
||||
const file = self.project.files.get(id).?;
|
||||
file.* = File{
|
||||
@ -1436,41 +1783,6 @@ pub fn addFile(self: *App, path: []const u8) !Id {
|
||||
return id;
|
||||
}
|
||||
|
||||
fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 {
|
||||
const byte_count = try file.getEndPos();
|
||||
if (byte_count % 8 != 0) {
|
||||
return error.NotMultipleOf8;
|
||||
}
|
||||
|
||||
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
|
||||
errdefer allocator.free(samples);
|
||||
|
||||
try file.seekTo(0);
|
||||
|
||||
var i: usize = 0;
|
||||
var buffer: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const count = try file.readAll(&buffer);
|
||||
if (count == 0) break;
|
||||
|
||||
for (0..@divExact(count, 8)) |j| {
|
||||
samples[i] = std.mem.bytesToValue(f64, buffer[(j*8)..]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
fn writeFileF64(file: std.fs.File, data: []const f64) !void {
|
||||
try file.seekTo(0);
|
||||
|
||||
for (data) |number| {
|
||||
const number_bytes = std.mem.asBytes(&number);
|
||||
try file.writeAll(number_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn loadFile(self: *App, id: Id) !void {
|
||||
const file = self.getFile(id) orelse return;
|
||||
const sample_list = self.project.sample_lists.get(file.samples_id) orelse return;
|
||||
@ -1483,10 +1795,12 @@ pub fn loadFile(self: *App, id: Id) !void {
|
||||
const samples_file = try cwd.openFile(file.path, .{ .mode = .read_only });
|
||||
defer samples_file.close();
|
||||
|
||||
const samples = try readFileF64(self.allocator, samples_file);
|
||||
defer self.allocator.free(samples);
|
||||
const byte_count = try samples_file.getEndPos();
|
||||
if (byte_count % 8 != 0) {
|
||||
return error.NotMultipleOf8;
|
||||
}
|
||||
|
||||
try self.project.appendSamples(self.allocator, file.samples_id, samples);
|
||||
try self.project.readSamplesFromFile(self.allocator, file.samples_id, samples_file, @divExact(byte_count, 8));
|
||||
}
|
||||
|
||||
// ---------------- Channels --------------------------------- //
|
||||
@ -1499,8 +1813,8 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id {
|
||||
const id = try self.project.channels.insertUndefined();
|
||||
errdefer self.project.channels.remove(id);
|
||||
|
||||
const sample_list_id = try self.project.sample_lists.insert(.{});
|
||||
errdefer self.project.sample_lists.remove(sample_list_id);
|
||||
const sample_list_id = try self.project.addSampleList(self.allocator);
|
||||
errdefer self.project.removeSampleList(sample_list_id);
|
||||
|
||||
const channel = self.project.channels.get(id).?;
|
||||
channel.* = Channel{
|
||||
@ -1574,10 +1888,12 @@ fn loadSavedSamples(self: *App, channel_id: Id) !void {
|
||||
const samples_file = try dir.openFile(saved_samples_location, .{ });
|
||||
defer samples_file.close();
|
||||
|
||||
const samples = try readFileF64(self.allocator, samples_file);
|
||||
defer self.allocator.free(samples);
|
||||
const byte_count = try samples_file.getEndPos();
|
||||
if (byte_count % 8 != 0) {
|
||||
return error.NotMultipleOf8;
|
||||
}
|
||||
|
||||
try self.project.appendSamples(self.allocator, channel.collected_samples_id, samples);
|
||||
try self.project.readSamplesFromFile(self.allocator, channel.collected_samples_id, samples_file, @divExact(byte_count, 8));
|
||||
|
||||
const stat = try dir.statFile(saved_samples_location);
|
||||
channel.last_sample_save_at = stat.mtime;
|
||||
@ -1646,23 +1962,12 @@ pub fn addView(self: *App, reference: View.Reference) !Id {
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn getViewSamples(self: *App, id: Id) []const f64 {
|
||||
return self.project.getViewSamples(id);
|
||||
}
|
||||
|
||||
pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache {
|
||||
const samples_list_id = self.project.getViewSampleList(id);
|
||||
const sample_list = self.project.sample_lists.get(samples_list_id).?;
|
||||
|
||||
return sample_list.graph_min_max_cache;
|
||||
}
|
||||
|
||||
fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
|
||||
const view = self.getView(id) orelse return;
|
||||
const sample_list_id = self.project.getViewSampleList(id);
|
||||
const sample_list_id = self.project.getViewSampleListId(id);
|
||||
const sample_list = self.project.sample_lists.get(sample_list_id).?;
|
||||
|
||||
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.samples.items.len));
|
||||
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength()));
|
||||
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| if (self.getChannel(channel_id)) |channel| {
|
||||
@ -1671,8 +1976,8 @@ fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
|
||||
view.available_y_range = RangeF64.init(allowed_sample_values.upper, allowed_sample_values.lower);
|
||||
},
|
||||
.file => {
|
||||
const min_sample = sample_list.min_sample orelse 0;
|
||||
const max_sample = sample_list.max_sample orelse 1;
|
||||
const min_sample = sample_list.min orelse 0;
|
||||
const max_sample = sample_list.max orelse 1;
|
||||
view.available_y_range = RangeF64.init(max_sample, min_sample);
|
||||
}
|
||||
}
|
||||
@ -1691,4 +1996,8 @@ pub fn loadView(self: *App, id: Id) !void {
|
||||
if (view.graph_opts.y_range.size() == 0) {
|
||||
view.graph_opts.y_range = view.available_y_range;
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = SampleList;
|
||||
}
|
@ -37,7 +37,6 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
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(.{
|
||||
@ -110,8 +109,9 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
|
||||
|
||||
|
||||
{ // Render graph
|
||||
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
|
||||
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
|
||||
const sample_list_id = app.project.getViewSampleListId(view_id);
|
||||
const sample_list = app.project.sample_lists.get(sample_list_id).?;
|
||||
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list);
|
||||
if (view.graph_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
|
@ -406,7 +406,6 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
}
|
||||
}
|
||||
} else if (ctx.view_controls.selected_tool == .select) {
|
||||
// TODO:
|
||||
|
||||
if (cursor != null) {
|
||||
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
|
||||
@ -418,10 +417,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
|
||||
const hold_end_mouse = view_range.remapTo(mouse_range, range.upper);
|
||||
const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse);
|
||||
if (signal.flags.contains(.left_released) and mouse_move_distance > 5) {
|
||||
if (view.appendMarkedRange(axis, range)) |marked_range| {
|
||||
marked_range.refresh(ctx.project.getViewSamples(view_id));
|
||||
}
|
||||
|
||||
_ = ctx.project.appendMarkedRange(view_id, axis, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
444
src/graph.zig
444
src/graph.zig
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const RangeF64 = @import("./range.zig").RangeF64;
|
||||
const SampleList = @import("./app.zig").SampleList;
|
||||
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const assert = std.debug.assert;
|
||||
@ -24,130 +25,6 @@ pub const ViewOptions = struct {
|
||||
y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
|
||||
color: rl.Color = srcery.red,
|
||||
|
||||
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 MinMaxCache = struct {
|
||||
const MinMaxPair = struct {
|
||||
min: f64,
|
||||
max: f64
|
||||
};
|
||||
|
||||
chunk_size: usize = 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(self: *MinMaxCache, chunk: []const f64) MinMaxPair {
|
||||
assert(chunk.len > 0);
|
||||
assert(chunk.len <= self.chunk_size);
|
||||
|
||||
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, self.chunk_size, self.chunk_size);
|
||||
while (iter.next()) |chunk| {
|
||||
try self.min_max_pairs.append(allocator, self.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, self.chunk_size);
|
||||
const to_chunk = @divFloor(samples.len - 1, self.chunk_size);
|
||||
|
||||
for (from_chunk..(to_chunk+1)) |i| {
|
||||
const chunk = samples[
|
||||
(self.chunk_size*i)..(@min(self.chunk_size*(i+1), samples.len))
|
||||
];
|
||||
const min_max_pair = self.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;
|
||||
}
|
||||
|
||||
test {
|
||||
const allocator = std.testing.allocator;
|
||||
var min_max_cache: MinMaxCache = .{ .chunk_size = 4 };
|
||||
defer min_max_cache.deinit(allocator);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = 1, .max = 2 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = 1, .max = 3 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
|
||||
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3, -1 });
|
||||
try std.testing.expectEqualSlices(
|
||||
MinMaxPair,
|
||||
&.{
|
||||
MinMaxPair{ .min = -1, .max = 3 }
|
||||
},
|
||||
min_max_cache.min_max_pairs.items
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const RenderCache = struct {
|
||||
@ -156,7 +33,6 @@ pub const RenderCache = struct {
|
||||
drawn_x_range: RangeF64
|
||||
};
|
||||
|
||||
min_max_cache: ?MinMaxCache = null,
|
||||
texture: ?rl.RenderTexture2D = null,
|
||||
key: ?Key = null,
|
||||
|
||||
@ -235,7 +111,164 @@ pub const SampleIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
// TODO: Wtf is this, I don't remember why I fiddled with the x range.
|
||||
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(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.lower > i_range.upper) {
|
||||
return;
|
||||
}
|
||||
|
||||
var block_id_iter = sample_list.blockIdIterator(
|
||||
@intFromFloat(i_range.lower),
|
||||
@intFromFloat(i_range.upper)
|
||||
);
|
||||
while (block_id_iter.next()) |block_id| {
|
||||
const block = sample_list.getBlock(block_id) orelse continue;
|
||||
const block_samples = block.samplesSlice();
|
||||
if (block_samples.len == 0) continue;
|
||||
|
||||
if (block_samples.len > 1) {
|
||||
for (0..(block_samples.len-1)) |i| {
|
||||
const i_f64: f64 = @floatFromInt(block_id * SampleList.Block.capacity + i);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i + 1])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (block_samples.len == SampleList.Block.capacity) {
|
||||
if (sample_list.getBlock(block_id + 1)) |next_block| {
|
||||
const next_block_samples = next_block.samplesSlice();
|
||||
if (next_block_samples.len > 0) {
|
||||
const i_f64: f64 = @floatFromInt((block_id + 1) * SampleList.Block.capacity - 1);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[SampleList.Block.capacity - 1])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, next_block_samples[0])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawColumn(draw_rect: rl.Rectangle, options: ViewOptions, sample_index: f64, column_min: f64, column_max: f64) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
const x = options.x_range.remapTo(draw_x_range, sample_index);
|
||||
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 drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
|
||||
assert(samples_per_column >= 1);
|
||||
|
||||
var sample_index = i_range.lower;
|
||||
while (sample_index < i_range.upper) : (sample_index += samples_per_column) {
|
||||
var column_min: f64 = std.math.inf(f64);
|
||||
var column_max: f64 = -std.math.inf(f64);
|
||||
|
||||
var iter = sample_list.iterator(
|
||||
@intFromFloat(sample_index),
|
||||
@intFromFloat(sample_index + samples_per_column + 1)
|
||||
);
|
||||
while (iter.next()) |segment| {
|
||||
for (segment) |sample| {
|
||||
column_min = @min(column_min, sample);
|
||||
column_max = @max(column_max, sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std.math.isInf(column_min)) {
|
||||
drawColumn(draw_rect, options, sample_index, column_min, column_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
|
||||
);
|
||||
if (i_range.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
|
||||
assert(samples_per_column >= @as(f64, @floatFromInt(SampleList.Block.capacity)));
|
||||
|
||||
var i = i_range.lower;
|
||||
while (i < i_range.upper) : (i += samples_per_column) {
|
||||
var column_min: f64 = std.math.inf(f64);
|
||||
var column_max: f64 = -std.math.inf(f64);
|
||||
|
||||
var block_id_iter = sample_list.blockIdIterator(
|
||||
@intFromFloat(i),
|
||||
@intFromFloat(i + samples_per_column)
|
||||
);
|
||||
while (block_id_iter.next()) |block_id| {
|
||||
const block = sample_list.getBlock(block_id) orelse continue;
|
||||
|
||||
if (block.min) |block_min| {
|
||||
column_min = @min(column_min, block_min);
|
||||
}
|
||||
|
||||
if (block.max) |block_max| {
|
||||
column_max = @max(column_max, block_max);
|
||||
}
|
||||
}
|
||||
|
||||
if (!std.math.isInf(column_min)) {
|
||||
drawColumn(draw_rect, options, i, column_min, column_max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(draw_rect.x),
|
||||
@intFromFloat(draw_rect.y),
|
||||
@ -244,157 +277,24 @@ fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []co
|
||||
);
|
||||
defer rl.endScissorMode();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const from_i: usize = @intFromFloat(i_range.lower);
|
||||
const to_i: usize = @intFromFloat(i_range.upper);
|
||||
if (to_i == 0 or from_i == to_i) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (from_i..(to_i-1)) |i| {
|
||||
const i_f64: f64 = @floatFromInt(i);
|
||||
rl.drawLineV(
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i])),
|
||||
},
|
||||
.{
|
||||
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
|
||||
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i + 1])),
|
||||
},
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
|
||||
|
||||
const i_range = options.x_range.intersectPositive(
|
||||
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
|
||||
);
|
||||
if (i_range.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samples_per_column = options.x_range.size() / draw_x_range.size();
|
||||
assert(samples_per_column >= 1);
|
||||
|
||||
var iter = SampleIterator.init(samples, i_range.lower, i_range.upper, samples_per_column);
|
||||
while (iter.next()) |column_samples| {
|
||||
var column_min = column_samples[0];
|
||||
var column_max = column_samples[0];
|
||||
|
||||
for (column_samples) |sample| {
|
||||
column_min = @min(column_min, sample);
|
||||
column_max = @max(column_max, sample);
|
||||
}
|
||||
|
||||
const x = options.x_range.remapTo(draw_x_range, iter.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 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.isNegative()) {
|
||||
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, min_max_cache.chunk_size);
|
||||
column_end = @divFloor(column_end, min_max_cache.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_cache: ?MinMaxCache) void {
|
||||
const x_range = options.x_range;
|
||||
|
||||
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
|
||||
if (x_range.lower >= @as(f64, @floatFromInt(sample_list.getLength()))) 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_cache != null and samples_per_column > @as(f64, @floatFromInt(2*min_max_cache.?.chunk_size))) {
|
||||
drawSamplesMinMax(draw_rect, options, min_max_cache.?);
|
||||
if (samples_per_column >= @as(f64, @floatFromInt(2*SampleList.Block.capacity))) {
|
||||
drawSamplesMinMax(draw_rect, options, sample_list);
|
||||
} else {
|
||||
drawSamplesApproximate(draw_rect, options, samples);
|
||||
drawSamplesApproximate(draw_rect, options, sample_list);
|
||||
}
|
||||
} else {
|
||||
drawSamplesExact(draw_rect, options, samples);
|
||||
drawSamplesExact(draw_rect, options, sample_list);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
||||
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, sample_list: *SampleList) void {
|
||||
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
|
||||
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
|
||||
|
||||
@ -423,7 +323,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
|
||||
|
||||
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)
|
||||
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range)
|
||||
};
|
||||
|
||||
if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) {
|
||||
@ -449,23 +349,19 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
|
||||
.width = render_size.x,
|
||||
.height = render_size.y
|
||||
};
|
||||
drawSamples(draw_rect, options, samples, cache.min_max_cache);
|
||||
drawSamples(draw_rect, options, sample_list);
|
||||
}
|
||||
|
||||
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
|
||||
if (draw_rect.width < 0 or draw_rect.height < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache != null and !disable_caching) {
|
||||
const c = cache.?;
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, sample_list);
|
||||
c.draw(draw_rect);
|
||||
} else {
|
||||
drawSamples(draw_rect, options, samples, null);
|
||||
drawSamples(draw_rect, options, sample_list, null);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = MinMaxCache;
|
||||
}
|
@ -66,7 +66,7 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||
.thread_safe = true
|
||||
.thread_safe = true,
|
||||
}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
@ -78,6 +78,8 @@ pub fn main() !void {
|
||||
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
|
||||
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
|
||||
|
||||
// std.debug.print("{any}\n", .{ std.mem.bytesAsSlice(f64, std.mem.sliceAsBytes(&[_]f64{ 1, 2, 3}) ) });
|
||||
|
||||
const icon_png = @embedFile("./assets/icon.png");
|
||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||
defer icon_image.unload();
|
||||
@ -183,11 +185,11 @@ pub fn main() !void {
|
||||
|
||||
rl.endDrawing();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("./ni-daq/root.zig");
|
||||
_ = @import("./range.zig");
|
||||
_ = @import("./graph.zig");
|
||||
_ = Application;
|
||||
}
|
@ -28,7 +28,7 @@ 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_sample_list_id: App.Id,
|
||||
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
|
||||
// Project settings
|
||||
@ -49,7 +49,8 @@ pub fn init(app: *App) !MainScreen {
|
||||
.amplitude_input = UI.TextInputStorage.init(allocator),
|
||||
.sample_rate_input = UI.TextInputStorage.init(allocator),
|
||||
.view_controls = ViewControlsSystem.init(&app.project),
|
||||
.sliding_window_input = UI.TextInputStorage.init(allocator)
|
||||
.sliding_window_input = UI.TextInputStorage.init(allocator),
|
||||
.preview_sample_list_id = try app.project.addSampleList(allocator)
|
||||
};
|
||||
|
||||
try self.frequency_input.setText("10");
|
||||
@ -59,12 +60,11 @@ pub fn init(app: *App) !MainScreen {
|
||||
}
|
||||
|
||||
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.sliding_window_input.deinit();
|
||||
self.app.project.removeSampleList(self.preview_sample_list_id);
|
||||
|
||||
self.clearProtocolErrorMessage();
|
||||
}
|
||||
@ -100,14 +100,15 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
||||
.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;
|
||||
if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
|
||||
const view_rect = Graph.ViewOptions{
|
||||
.x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())),
|
||||
.y_range = self.preview_samples_y_range
|
||||
};
|
||||
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, sample_list);
|
||||
if (self.protocol_graph_cache.texture) |texture| {
|
||||
protocol_view.texture = texture.texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,9 +183,17 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
|
||||
}
|
||||
|
||||
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.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
|
||||
var preview_samples: std.ArrayListUnmanaged(f64) = .{};
|
||||
defer preview_samples.deinit(allocator);
|
||||
|
||||
try App.Channel.generateSine(&preview_samples, allocator, sample_rate, frequency, amplitude);
|
||||
sample_list.clear(allocator);
|
||||
try sample_list.append(preview_samples.items);
|
||||
|
||||
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
|
||||
self.protocol_graph_cache.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (self.protocol_error_message) |message| {
|
||||
@ -319,7 +328,6 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
.label = "Sync controls"
|
||||
});
|
||||
|
||||
const samples = project.getViewSamples(view_id);
|
||||
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| {
|
||||
@ -363,24 +371,27 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
self.app.pushCommand(.{ .reload_file = file_id });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = ui.label("Samples: {d}", .{ samples.len });
|
||||
const sample_list_id = project.getViewSampleListId(view_id);
|
||||
const sample_list = project.sample_lists.get(sample_list_id).?;
|
||||
const sample_count = sample_list.getLength();
|
||||
|
||||
_ = ui.label("Samples: {d}", .{ sample_count });
|
||||
|
||||
var duration_str: []const u8 = "-";
|
||||
if (sample_rate != null) {
|
||||
const duration = @as(f64, @floatFromInt(samples.len)) / sample_rate.?;
|
||||
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 });
|
||||
|
||||
_ = try ui.numberInput(f64, .{
|
||||
try project.updateSlidingWindow(view_id, self.app.allocator, try ui.numberInput(f64, .{
|
||||
.key = ui.keyFromString("Sliding window"),
|
||||
.storage = &self.sliding_window_input
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
|
||||
|
Loading…
Reference in New Issue
Block a user