diff --git a/src/main.zig b/src/main.zig index ff44ac4..a932155 100644 --- a/src/main.zig +++ b/src/main.zig @@ -656,14 +656,25 @@ fn wlKeyboardHandleKey(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, serial: u32 c.XKB_KEY_Escape => { wl_state.running = false; }, - c.XKB_KEY_space => { - wl_state.app_input.key.space.down = pressed; + c.XKB_KEY_f => { + const already_pressed: bool = wl_state.app_input.key.f.down; + wl_state.app_input.key.f.down = pressed; + wl_state.app_input.key.f.pressing = pressed and already_pressed; }, c.XKB_KEY_r => { + const already_pressed: bool = wl_state.app_input.key.r.down; wl_state.app_input.key.r.down = pressed; + wl_state.app_input.key.r.pressing = pressed and already_pressed; }, c.XKB_KEY_q => { + const already_pressed: bool = wl_state.app_input.key.q.down; wl_state.app_input.key.q.down = pressed; + wl_state.app_input.key.q.pressing = pressed and already_pressed; + }, + c.XKB_KEY_space => { + const already_pressed: bool = wl_state.app_input.key.space.down; + wl_state.app_input.key.space.down = pressed; + wl_state.app_input.key.space.pressing = pressed and already_pressed; }, else => {}, } @@ -929,13 +940,15 @@ pub fn main() u8 { const render_start_time: u64 = timer.read(); + wl_state.app_input.delta_time = frame_target_time_s; + const app_offscreen_buffer: puzzle.platform.OffscreenBuffer = .{ .width = wl_state.window_width, .height = wl_state.window_height, .stride = wl_state.window_width * 4, .data = shared_memory_pool_data, }; - puzzle.updateAndRender(&app_memory, app_offscreen_buffer, wl_state.app_input); + puzzle.updateAndRender(&app_memory, app_offscreen_buffer, &wl_state.app_input); const render_end_time: u64 = timer.read(); const render_time = render_end_time - render_start_time; diff --git a/src/platform.zig b/src/platform.zig index 3225dec..43ddc16 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -8,17 +8,23 @@ pub fn megaBytes(bytes: usize) usize { return kiloBytes(bytes) * 1024; } -const Key = struct { +pub const Key = struct { down: bool, + pressing: bool, + frame_count: u32, }; const Keyboard = struct { + esc: Key, + f: Key, r: Key, q: Key, space: Key, }; pub const AppInput = struct { + delta_time: f64, + mouse_present: bool = false, mouse_left_down: bool = false, mouse_x: f64, diff --git a/src/root.zig b/src/root.zig index 7596168..34d4c13 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,5 +1,6 @@ //! By convention, root.zig is the root source file when making a library. const std = @import("std"); +const builtin = @import("builtin"); pub const platform = @import("platform.zig"); @@ -9,6 +10,9 @@ const V2 = struct { x: f32, y: f32, + const max = V2{ .x = std.math.floatMax(f32), .y = std.math.floatMax(f32) }; + const zero = V2{ .x = 0.0, .y = 0.0 }; + fn add(self: V2, other: V2) V2 { return .{ .x = self.x + other.x, @@ -24,6 +28,12 @@ const V2 = struct { } }; +const BBox = struct { + min: V2, + max: V2, + center: V2, +}; + const Camera = struct { pos: V2, offset: V2, @@ -33,8 +43,27 @@ const Brick = struct { rotating: bool, pos: V2, color: u32, + rotating_time: f64, tiles: [4][4]u32, + fn transpose(self: Brick) [4][4]u32 { + var result = [4][4]u32{ + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + }; + + for (0..self.tiles.len) |i| { + const row = self.tiles[i]; + for (0..row.len) |j| { + result[i][j] = self.tiles[j][i]; + } + } + + return result; + } + fn rotate(self: Brick) [4][4]u32 { var result = [4][4]u32{ .{ 0, 0, 0, 0 }, @@ -61,6 +90,11 @@ const State = struct { bricks: [8]Brick, }; +const tile_size = V2{ + .x = TILE_SIZE, + .y = TILE_SIZE, +}; + fn pointInsideRect(p: V2, rect_start: V2, rect_size: V2) bool { var result: bool = false; @@ -74,6 +108,67 @@ fn pointInsideRect(p: V2, rect_start: V2, rect_size: V2) bool { return result; } +fn findBrickBoundingBox(brick: *const Brick) BBox { + var result = BBox{ .min = V2.max, .max = V2.zero, .center = V2.zero }; + + for (brick.tiles, 0..) |row, i| { + for (row, 0..) |tile, j| { + if (tile > 0) { + const tile_start = V2{ + .x = @floatFromInt(j * TILE_SIZE), + .y = @floatFromInt(i * TILE_SIZE), + }; + const tile_end = tile_start.add(tile_size); + + const start = brick.pos.add(tile_start); + const end = brick.pos.add(tile_end); + + if (start.x < result.min.x) result.min.x = start.x; + if (start.y < result.min.y) result.min.y = start.y; + if (result.max.x < end.x) result.max.x = end.x; + if (result.max.y < end.y) result.max.y = end.y; + } + } + } + + result.center = .{ + .x = (result.min.x + result.max.x) / 2.0, + .y = (result.min.y + result.max.y) / 2.0, + }; + + return result; +} + +fn drawRect(buffer: platform.OffscreenBuffer, pos: V2, size: V2, color: u32, line_width: i32) void { + var pixels: []u32 = @ptrCast(@alignCast(buffer.data)); + + const start_x: i32 = if (pos.x < 0) 0 else @intFromFloat(pos.x); + const start_y: i32 = if (pos.y < 0) 0 else @intFromFloat(pos.y); + + const end = pos.add(size); + const end_x: i32 = if (end.x < 0) 0 else @intFromFloat(end.x); + const end_y: i32 = if (end.y < 0) 0 else @intFromFloat(end.y); + + const min_x: usize = @intCast(if (buffer.width < start_x) buffer.width else start_x); + const min_y: usize = @intCast(if (buffer.height < start_y) buffer.height else start_y); + const max_x: usize = @intCast(if (buffer.width < end_x) buffer.width else end_x); + const max_y: usize = @intCast(if (buffer.height < end_y) buffer.height else end_y); + + for (min_y..max_y) |y| { + for (min_x..max_x) |x| { + const left = x < start_x + line_width; + const top = y < start_y + line_width; + const right = end_x - line_width - 1 < x; + const bottom = end_y - line_width - 1 < y; + if (left or top or right or bottom) { + const idx: usize = y * @as(usize, @intCast(buffer.width)) + x; + + pixels[idx] = color; + } + } + } +} + fn fillRect( buffer: platform.OffscreenBuffer, pos_x: i32, @@ -91,6 +186,11 @@ fn fillRect( if (start_x < 0) start_x = 0; if (start_y < 0) start_y = 0; + if (buffer.width < start_x) start_x = buffer.width; + if (buffer.height < start_y) start_y = buffer.height; + + if (end_x < 0) end_x = 0; + if (end_y < 0) end_y = 0; if (buffer.width < end_x) end_x = buffer.width; if (buffer.height < end_y) end_y = buffer.height; @@ -113,7 +213,22 @@ fn fillSquare( fillRect(buffer, x, y, size, size, color); } -pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBuffer, input: platform.AppInput) void { +fn keyPressed(key: *platform.Key) bool { + var result = false; + + if (key.down) { + if (key.frame_count == 0) { + result = true; + } + key.frame_count += 1; + } else { + key.frame_count = 0; + } + + return result; +} + +pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBuffer, input: *platform.AppInput) void { var state: *State = @ptrCast(@alignCast(memory.permanent_storage)); const mid = V2{ @@ -125,6 +240,7 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[0] = Brick{ .rotating = false, + .rotating_time = 0, .pos = V2{ .x = 0, .y = 50 }, .color = 0xFFFF0000, .tiles = [4][4]u32{ @@ -137,8 +253,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[1] = Brick{ .rotating = false, - .pos = V2{ .x = 32, .y = 50 }, - .color = 0xFFEF0000, + .rotating_time = 0, + .pos = V2{ .x = 256, .y = 50 }, + .color = 0xFF665566, .tiles = [4][4]u32{ .{ 2, 2, 2, 0 }, .{ 2, 2, 0, 0 }, @@ -149,8 +266,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[2] = Brick{ .rotating = false, - .pos = V2{ .x = 64, .y = 50 }, - .color = 0xFFDF0000, + .rotating_time = 0, + .pos = V2{ .x = 384, .y = 50 }, + .color = 0xFF33A000, .tiles = [4][4]u32{ .{ 3, 3, 3, 0 }, .{ 0, 0, 3, 3 }, @@ -161,8 +279,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[3] = Brick{ .rotating = false, - .pos = V2{ .x = 96, .y = 50 }, - .color = 0xFFCF0000, + .rotating_time = 0, + .pos = V2{ .x = 512, .y = 50 }, + .color = 0xFFCF6666, .tiles = [4][4]u32{ .{ 4, 0, 0, 0 }, .{ 4, 4, 4, 0 }, @@ -173,8 +292,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[4] = Brick{ .rotating = false, - .pos = V2{ .x = 128, .y = 150 }, - .color = 0xFFBF0000, + .rotating_time = 0, + .pos = V2{ .x = 384, .y = 150 }, + .color = 0xFF00FFAA, .tiles = [4][4]u32{ .{ 5, 5, 5, 5 }, .{ 0, 5, 0, 0 }, @@ -185,8 +305,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[5] = Brick{ .rotating = false, + .rotating_time = 0, .pos = V2{ .x = 160, .y = 250 }, - .color = 0xFFAF0000, + .color = 0xFFAF0F0F, .tiles = [4][4]u32{ .{ 6, 6, 6, 0 }, .{ 6, 0, 0, 0 }, @@ -197,8 +318,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[6] = Brick{ .rotating = false, + .rotating_time = 0, .pos = V2{ .x = 192, .y = 350 }, - .color = 0xFF9F0000, + .color = 0xFF9FFF00, .tiles = [4][4]u32{ .{ 7, 7, 7, 0 }, .{ 7, 7, 7, 0 }, @@ -209,8 +331,9 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.bricks[7] = Brick{ .rotating = false, + .rotating_time = 0, .pos = V2{ .x = 224, .y = 450 }, - .color = 0xFF8F0000, + .color = 0xFF8F00FF, .tiles = [4][4]u32{ .{ 8, 8, 8, 0 }, .{ 8, 0, 8, 0 }, @@ -225,14 +348,17 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu // Update const mouse_pos = V2{ .x = @floatCast(input.mouse_x), .y = @floatCast(input.mouse_y) }; - const tile_size = V2{ - .x = TILE_SIZE, - .y = TILE_SIZE, - }; + const mouse_world_pos = mouse_pos.sub(state.camera.pos); var mouse_on_brick = false; for (&state.bricks) |*brick| { - brick.rotating = false; + if (brick.rotating) { + brick.rotating_time += input.delta_time; + if (brick.rotating_time > 0.25) { + brick.rotating = false; + brick.rotating_time = 0.0; + } + } for (brick.tiles, 0..) |row, i| { for (row, 0..) |tile, j| { @@ -249,11 +375,25 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu const diff = mouse_pos.sub(state.mouse_pos); brick.pos = brick.pos.add(diff); - if (input.key.space.down and !brick.rotating) { - // TODO: Rotate + if (keyPressed(&input.key.space)) { brick.rotating = true; const rotated = brick.rotate(); brick.tiles = rotated; + + const bbox = findBrickBoundingBox(brick); + const center = brick.pos.sub(bbox.center); + brick.pos = mouse_world_pos.add(center); + } + + // Flip + if (keyPressed(&input.key.f)) { + brick.rotating = true; + const flipped = brick.transpose(); + brick.tiles = flipped; + + const bbox = findBrickBoundingBox(brick); + const center = brick.pos.sub(bbox.center); + brick.pos = mouse_world_pos.add(center); } mouse_on_brick = true; @@ -294,6 +434,8 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu } for (state.bricks) |brick| { + const bounding_box = findBrickBoundingBox(&brick); + for (brick.tiles, 0..) |row, i| { for (row, 0..) |tile, j| { if (tile > 0) { @@ -307,11 +449,34 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu @intFromFloat(camera_pos.x), @intFromFloat(camera_pos.y), 30, - if (brick.rotating) 0xFF00FF00 else brick.color , + if (brick.rotating) 0xFF00FF00 else brick.color, ); } } } + + if (comptime builtin.mode == std.builtin.OptimizeMode.Debug) { + { + const camera_pos = brick.pos.add(state.camera.pos); + drawRect(buffer, camera_pos, .{ .x = 4 * TILE_SIZE, .y = 4 * TILE_SIZE }, 0xFFFF0000, 1); + } + + { + const bbox_size = bounding_box.max.sub(bounding_box.min); + const camera_pos = bounding_box.min.add(state.camera.pos); + + drawRect(buffer, camera_pos, bbox_size, 0xFFFF0000, 1); + } + + { + const size = 8; + const half = @divTrunc(size, 2); + const camera_pos = bounding_box.center.add(state.camera.pos); + const x: i32 = @intFromFloat(camera_pos.x); + const y: i32 = @intFromFloat(camera_pos.y); + fillSquare(buffer, x - half, y - half, 8, 0xFFFF0000); + } + } } state.frame_count += 1;