diff --git a/extras/ingrid/LICENSE b/extras/ingrid/LICENSE new file mode 100644 index 0000000..96bab1d --- /dev/null +++ b/extras/ingrid/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Lars Pontoppidan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extras/ingrid/examples/render.v b/extras/ingrid/examples/render.v new file mode 100644 index 0000000..7e15310 --- /dev/null +++ b/extras/ingrid/examples/render.v @@ -0,0 +1,399 @@ +// Copyright(C) 2024 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module main + +import shy.lib as shy +import shy.vec +import shy.embed +import shy.extras.ingrid + +const c_max_zoom = 3 +const c_min_zoom = 0.1 + +fn main() { + mut app := &App{} + shy.run[App](mut app)! +} + +@[heap] +pub struct App { + embed.ExampleApp +mut: + grid ingrid.Grid2D + config u8 + configs [2]ingrid.Config2D + bookmark ingrid.Bookmark2D + viewport [2]shy.Rect + warp_to vec.Vec2[f32] + warp_anchor shy.Anchor = .center + zoom f32 = 1.0 + clip_viewport bool +} + +@[markused] +pub fn (mut a App) init() ! { + a.ExampleApp.init()! + + canvas := a.window.canvas() + + a.viewport[0] = shy.Rect{ + x: shy.quarter * canvas.width + y: shy.quarter * canvas.height + width: shy.half * canvas.width + height: shy.half * canvas.height + } + + a.configs[0] = ingrid.Config2D{ + id: 0 + cell_size: shy.Size{100, 100} + fill_multiply: shy.Size{1, 1} + dimensions: a.viewport[0].size() + } + + a.viewport[1] = shy.Rect{ + x: 0 + y: 0 + width: canvas.width + height: canvas.height + } + a.configs[1] = ingrid.Config2D{ + id: 1 + cell_size: shy.Size{ + width: 1024 + height: 1024 + } + fill_multiply: shy.Size{ + width: 10 + height: 8 + } + dimensions: a.viewport[1].size() + } + + config := a.configs[a.config] or { a.configs[0] } + a.update_grid_config(config) +} + +pub fn (mut a App) update_viewport_dimensions() { + canvas := a.window.canvas() + + a.viewport[0] = shy.Rect{ + x: shy.quarter * canvas.width + y: shy.quarter * canvas.height + width: shy.half * canvas.width + height: shy.half * canvas.height + } + + a.configs[0] = ingrid.Config2D{ + ...a.configs[0] + dimensions: a.viewport[0].size() + } + + a.viewport[1] = shy.Rect{ + x: 0 + y: 0 + width: canvas.width + height: canvas.height + } + a.configs[1] = ingrid.Config2D{ + ...a.configs[1] + dimensions: a.viewport[1].size() + } +} + +pub fn (mut a App) update_grid_config(config ingrid.Config2D) { + a.grid = ingrid.make_2d(config) + a.grid.init() + a.grid.warp_anchor(a.warp_to, a.warp_anchor) +} + +@[markused] +pub fn (mut a App) event(e shy.Event) { + a.ExampleApp.event(e) + mut fbf := a.configs[a.config].fill_multiply + mut cs := a.configs[a.config].cell_size + match e { + shy.KeyEvent { + if e.state == .up { + return + } + key := e.key_code + kb := a.keyboard + alt_is_held := (kb.is_key_down(.lalt) || kb.is_key_down(.ralt)) + ctrl_is_held := (kb.is_key_down(.lctrl) || kb.is_key_down(.rctrl)) + shift_is_held := (kb.is_key_down(.lshift) || kb.is_key_down(.rshift)) + move_by := f32(22.0) + match key { + .up { + if shift_is_held { + fbf.height++ + } else if alt_is_held { + cs.height -= 10 + } else if ctrl_is_held { + a.warp_to.y -= 1 + } else { + a.grid.move(shy.vec2[f32](0, move_by)) + } + } + .left { + if shift_is_held { + fbf.width-- + } else if alt_is_held { + cs.width -= 10 + } else if ctrl_is_held { + a.warp_to.x -= 1 + } else { + a.grid.move(shy.vec2[f32](move_by, 0)) + } + } + .right { + if shift_is_held { + fbf.width++ + } else if alt_is_held { + cs.width += 10 + } else if ctrl_is_held { + a.warp_to.x += 1 + } else { + a.grid.move(shy.vec2[f32](-move_by, 0)) + } + } + .down { + if shift_is_held { + fbf.height-- + } else if alt_is_held { + cs.height += 10 + } else if ctrl_is_held { + a.warp_to.y += 1 + } else { + a.grid.move(shy.vec2[f32](0, -move_by)) + } + } + .w { + a.grid.warp_anchor(a.warp_to, a.warp_anchor) + } + .s { + a.bookmark = a.grid.bookmark() + } + .l { + a.grid.load_bookmark(a.bookmark) + } + .a { + if shift_is_held { + a.warp_anchor = a.warp_anchor.prev() + } else { + a.warp_anchor = a.warp_anchor.next() + } + } + .v { + a.clip_viewport = !a.clip_viewport + } + .c { + mut nc := int(a.config) + if shift_is_held { + nc++ + } else { + nc-- + } + if nc < 0 { + nc = a.configs.len - 1 + } + if nc > a.configs.len - 1 { + nc = 0 + } + a.config = u8(nc) + a.update_grid_config(a.configs[a.config]) + } + else {} + } + + if key in [.up, .left, .right, .down] && (alt_is_held || shift_is_held) { + a.configs[a.config] = ingrid.Config2D{ + ...a.configs[a.config] + cell_size: cs + fill_multiply: fbf + } + a.update_grid_config(a.configs[a.config]) + } + } + shy.MouseWheelEvent { + if e.scroll_y > 0 { + a.zoom += 0.1 + } else { + a.zoom -= 0.1 + } + + if a.zoom > c_max_zoom { + a.zoom = c_max_zoom + } + if a.zoom < c_min_zoom { + a.zoom = c_min_zoom + } + } + shy.WindowResizeEvent { + a.update_viewport_dimensions() + a.update_grid_config(a.configs[a.config]) + } + else {} + } +} + +@[markused] +pub fn (mut a App) frame(dt f64) { + canvas_width := a.window.canvas().width + canvas_height := a.window.canvas().height + + center := shy.vec2[f32](canvas_width, canvas_height).mul_scalar(shy.half) + + draw := a.shy.draw() + draw.push_matrix() + draw.translate(center.x, center.y, 0) + draw.scale(a.zoom, a.zoom, 1) + draw.translate(-center.x, -center.y, 0) + + cell_size := a.grid.config.cell_size + viewport := a.viewport[a.config] + viewport_offset := viewport.pos_as_vec2[f32]() + + for i in 0 .. a.grid.count_cells() { + cell := a.grid.cell_at_index(i) + cell_pos := cell.pos + viewport_offset + cell_rect := shy.Rect{ + x: cell_pos.x + y: cell_pos.y + width: cell_size.width + height: cell_size.height + } + + if a.clip_viewport && !cell_rect.hit_rect(viewport) { + continue + } + + mut color := shy.colors.red + if cell.ixy == a.warp_to { + color = shy.colors.blue + } + if cell_rect.scale_at(center.x, center.y, a.zoom, a.zoom).contains(a.mouse.x, + a.mouse.y) + { + color = color.copy_set_a(127) + } + + a.quick.rect( + x: cell_pos.x + y: cell_pos.y + width: cell_size.width + height: cell_size.height + color: color + ) + a.quick.text( + x: cell_pos.x + (cell_size.width * 0.5) + y: cell_pos.y + (cell_size.height * 0.5) + origin: shy.Anchor.center + text: '${cell.ixy.x},${cell.ixy.y}' + ) + } + + fill := a.grid.fill() + bounds := a.grid.bounds() + + a.quick.rect( + x: fill.x + viewport_offset.x + y: fill.y + viewport_offset.y + width: fill.width + height: fill.height + fills: .stroke + stroke: shy.Stroke{ + color: shy.colors.red + } + ) + a.quick.rect( + x: bounds.x + viewport_offset.x + y: bounds.y + viewport_offset.y + width: bounds.width + height: bounds.height + fills: .stroke + stroke: shy.Stroke{ + color: shy.colors.blue + } + ) + + a.quick.rect( + x: viewport_offset.x + y: viewport_offset.y + width: viewport.width + height: viewport.height + fills: .stroke + stroke: shy.Stroke{ + color: shy.colors.green + } + ) + + a.quick.circle( + x: center.x + y: center.y + radius: 10 + fills: .body + color: shy.colors.yellow.copy_set_a(127) + origin: shy.Anchor.center + ) + + draw.pop_matrix() + + a.quick.text( + x: canvas_width * 0.01 + y: canvas_height * 0.01 + origin: shy.Anchor.top_left + size: 28 + text: 'Controls +Window can be resized via the mouse. +Zoom canvas via mouse scroll wheel. +Use "Alt" + arrow keys to change cell size. +Use "Shift" + arrow keys to change fill multipliers. +Use arrow keys alone to move origin cell. +Use "Ctrl" + arrow keys to adjust warp point. +Use "w" to warp the grid to warp point. +Use "Shift" + "a" and "a" to adjust the warp anchor. +Use "s" to bookmark (save) current grid. +Use "l" to load the last saved bookmark. +Use "c" to change current grid config +Use "v" to change viewport clipping (optimization)' + ) + + a.quick.text( + x: canvas_width * 0.01 + y: canvas_height * 0.99 + origin: shy.Anchor.bottom_left + size: 28 + text: 'Window --- +Size: ${a.window.width}x${a.window.height} + +Mouse --- +Location: ${a.mouse.x},${a.mouse.y} + +Canvas --- +Factor: ${a.window.canvas().factor} +Size: ${a.window.canvas().width}x${a.window.canvas().height} +Zoom: ${a.zoom} + +Config --- +Cell size: ${cell_size.width}x${cell_size.height} +Fill multipliers: ${a.grid.config.fill_multiply.width},${a.grid.config.fill_multiply.height} +Dimensions: ${a.grid.config.dimensions.width}x${a.grid.config.dimensions.height} + +Grid --- +Origin: ixy: ${a.grid.origin().ixy.x},${a.grid.origin().ixy.y} pos: ${a.grid.origin().pos.x},${a.grid.origin().pos.y} +Cols (x): ${a.grid.cols()} +Rows (y): ${a.grid.rows()} +Cells: ${a.grid.count_cells()} +Fill: ${a.grid.fill().x},${a.grid.fill().y} ${a.grid.fill().width}x${a.grid.fill().height} +Bounds: ${a.grid.bounds().x},${a.grid.bounds().y} ${a.grid.bounds().width}x${a.grid.bounds().height} + +Bookmark --- +ixy: ${a.bookmark.ixy.x},${a.bookmark.ixy.y} +pos: ${a.bookmark.pos.x},${a.bookmark.pos.y} + +Warp --- +To: ${a.warp_to.x},${a.warp_to.y} +Anchor: ${a.warp_anchor}' + ) +} diff --git a/extras/ingrid/grid2d.v b/extras/ingrid/grid2d.v new file mode 100644 index 0000000..b54ff9b --- /dev/null +++ b/extras/ingrid/grid2d.v @@ -0,0 +1,369 @@ +module ingrid + +// 2D grid in 1D array: https://softwareengineering.stackexchange.com/questions/212808/treating-a-1d-data-structure-as-2d-grid +import shy.lib as shy +import shy.vec +import shy.mth + +const c_zero_vec2 = vec.Vec2[f32]{0, 0} + +@[if shy_trace_ingrid ?] +fn epln(str string) { + eprintln(str) +} + +pub struct Cell2D { +pub: + grid_id u32 + ixy vec.Vec2[f32] + pos vec.Vec2[f32] +} + +pub struct Bookmark2D { +pub: + config Config2D + ixy vec.Vec2[f32] + pos vec.Vec2[f32] +} + +@[params] +pub struct Config2D { +pub: + id u32 //@[required] + units vec.Vec2[f32] = vec.Vec2[f32]{1, 1} // Units or resolution of the cell coordinates + cell_size shy.Size = shy.Size{100, 100} // Default cell size w,h + fill_multiply shy.Size = shy.Size{1, 1} + dimensions shy.Size +} + +pub struct Grid2D { +pub: + config Config2D // [required] +mut: + origin Cell2D // The "origin" (top-left) cell that everything can be calculated from + + fill shy.Rect // Area which should be "filled" with cells at a minimum + bounds shy.Rect // Cells shift ixy coordinates when moving outside these + + rows int + cols int +} + +pub fn make_2d(config Config2D) Grid2D { + return Grid2D{ + config: config + } +} + +pub fn (g &Grid2D) origin() Cell2D { + return g.origin +} + +// cols_and_rows returns the amount of columns and rows used in the grid +pub fn (g &Grid2D) cols_and_rows() (int, int) { + return g.cols, g.rows +} + +// cols returns the amount of columns in the grid +pub fn (g &Grid2D) cols() int { + return g.cols +} + +// rows returns the amount of rows used in the grid +pub fn (g &Grid2D) rows() int { + return g.rows +} + +pub fn (g &Grid2D) count_cells() int { + return g.cols * g.rows +} + +pub fn (g &Grid2D) fill() shy.Rect { + return shy.Rect{ + x: g.fill.x + y: g.fill.y + width: g.fill.width + height: g.fill.height + } +} + +pub fn (g &Grid2D) bounds() shy.Rect { + return shy.Rect{ + x: g.bounds.x + y: g.bounds.y + width: g.bounds.width + height: g.bounds.height + } +} + +// init (re)initializes the grid with cells when called. +pub fn (mut g Grid2D) init() { + g.update_dimensions() + g.reset_origin() + g.warp(vec.Vec2[f32]{}) +} + +// shutdown shuts down the grid. +pub fn (mut g Grid2D) shutdown() {} + +pub fn (mut g Grid2D) reset_origin() { + g.origin = Cell2D{ + grid_id: g.config.id + ixy: c_zero_vec2 + pos: c_zero_vec2 + } +} + +pub fn (g &Grid2D) cell_at_index_safe(index int) Cell2D { + mut i := index + if i >= g.count_cells() { + i = g.count_cells() - 1 + } else if i < 0 { + i = 0 + } + ix := i % g.cols + iy := (i / g.cols) % g.rows // NOTE: integer division + + ixy := vec.Vec2[f32]{g.origin.ixy.x + ix, g.origin.ixy.y + iy} * g.config.units + pos := vec.Vec2[f32]{g.origin.pos.x + (ix * g.config.cell_size.width), g.origin.pos.y + + (iy * g.config.cell_size.height)} + + return Cell2D{ + grid_id: g.config.id + ixy: ixy + pos: pos + } +} + +pub fn (g &Grid2D) cell_at_index(index int) Cell2D { + $if !no_bounds_checking { + if index < 0 || index >= g.count_cells() { + panic('shy.extras.ingrid.Grid2D.cell_at_index/1:\nRequested index must be in range 0 to (Grid2D.cols * Grid2D.rows - 1) (Grid2D.count_cells/0).\nUse `cell_at_index_safe/1` for a slightly slower, but safe access') + } + } + ix := index % g.cols + iy := (index / g.cols) % g.rows // NOTE: integer division + + ixy := vec.Vec2[f32]{g.origin.ixy.x + ix, g.origin.ixy.y + iy} * g.config.units + pos := vec.Vec2[f32]{g.origin.pos.x + (ix * g.config.cell_size.width), g.origin.pos.y + + (iy * g.config.cell_size.height)} + + return Cell2D{ + grid_id: g.config.id + ixy: ixy + pos: pos + } +} + +// move moves origin cell relatively by `relative_v`. +pub fn (mut g Grid2D) move(relative_v vec.Vec2[f32]) { + if relative_v.x == 0 && relative_v.y == 0 { + return + } + mut v := relative_v + // Move in smaller bits + for mth.abs(v.x) > g.config.cell_size.width || mth.abs(v.y) > g.config.cell_size.height { + if v.x > g.config.cell_size.width { + v.x -= g.config.cell_size.width + g.move(vec.Vec2[f32]{g.config.cell_size.width, 0}) + } else if v.x < -g.config.cell_size.width { + v.x += g.config.cell_size.width + g.move(vec.Vec2[f32]{-g.config.cell_size.width, 0}) + } + + if v.y > g.config.cell_size.height { + v.y -= g.config.cell_size.height + g.move(vec.Vec2[f32]{0, g.config.cell_size.height}) + } else if v.y < -g.config.cell_size.height { + v.y += g.config.cell_size.height + g.move(vec.Vec2[f32]{0, -g.config.cell_size.height}) + } + // println('relative_v ${relative_v.x},${relative_v.y} require smaller move ${v.x},${v.y} ') + } + + mut nx := g.origin.pos.x + v.x + mut ny := g.origin.pos.y + v.y + mut nix := g.origin.ixy.x + mut niy := g.origin.ixy.y + + v_br := g.cell_at_index(g.count_cells() - 1) // TODO: can assert for < 0 + v_br_nx := v_br.pos.x + v.x + v_br_ny := v_br.pos.y + v.y + + limit_tl := vec.Vec2[f32]{g.bounds.x, g.bounds.y} + limit_br := vec.Vec2[f32]{g.bounds.x + g.bounds.width - g.config.cell_size.width, g.bounds.y + + g.bounds.height - g.config.cell_size.height} + + if nx < limit_tl.x { + nx = nx + g.config.cell_size.width + nix = nix + (1 * g.config.units.x) + } else if v_br_nx > limit_br.x { + nx = nx - g.config.cell_size.width + nix = nix - (1 * g.config.units.x) + } + if ny < limit_tl.y { + ny = ny + g.config.cell_size.height + niy = niy + (1 * g.config.units.y) + } else if v_br_ny > limit_br.y { + ny = ny - g.config.cell_size.height + niy = niy - (1 * g.config.units.y) + } + + g.origin = Cell2D{ + grid_id: g.config.id + ixy: vec.Vec2[f32]{nix, niy} + pos: vec.Vec2[f32]{nx, ny} + } +} + +// warp instantly "warps" to this cell coordinate, and centers the cell inside the dimensions. +pub fn (mut g Grid2D) warp(ixy vec.Vec2[f32]) { + g.warp_anchor(ixy, .center) +} + +// warp_anchor instantly "warps" to cell coordinate `ixy` making it appear at the `anchor` inside the dimensions. +pub fn (mut g Grid2D) warp_anchor(ixy vec.Vec2[f32], anchor shy.Anchor) { + g.reset_origin() + g.origin = Cell2D{ + ...g.origin + ixy: ixy + } + + mut move_to := vec.Vec2[f32]{0, 0} + + cell_size := g.config.cell_size + half_cell_size := cell_size.mul_scalar(0.5) + match anchor { + .top_left { + // Default / origin point + } + .top_center { + move_to.x = (g.config.dimensions.width * 0.5) - half_cell_size.width + } + .top_right { + move_to.x = (g.config.dimensions.width) - cell_size.width + } + .center_left { + move_to.y = (g.config.dimensions.height * 0.5) - half_cell_size.height + } + .center { + move_to.x = (g.config.dimensions.width * 0.5) - half_cell_size.width + move_to.y = (g.config.dimensions.height * 0.5) - half_cell_size.height + } + .center_right { + move_to.x = (g.config.dimensions.width) - cell_size.width + move_to.y = (g.config.dimensions.height * 0.5) - half_cell_size.height + } + .bottom_left { + move_to.y = (g.config.dimensions.height) - cell_size.height + } + .bottom_center { + move_to.x = (g.config.dimensions.width * 0.5) - half_cell_size.width + move_to.y = (g.config.dimensions.height) - cell_size.height + } + .bottom_right { + move_to.x = (g.config.dimensions.width) - cell_size.width + move_to.y = (g.config.dimensions.height) - cell_size.height + } + } + + g.move(move_to) + for _ in 0 .. mth.max(g.cols, g.rows) { + g.touch() + } +} + +// cell_at gets the cell at dimensions (self.rect) x,y coordinate. +// pub fn (g &Grid2D) cell_at(xy vec.Vec2[f32]) ?Cell2D { +// vp := g.config.dimensions +// +// if xy.x < 0 || xy.x > vp.width { +// return none +// } +// if xy.y < 0 || xy.y > vp.height { +// return none +// } +// +// for i in 0 .. g.count_cells() { +// cell := g.cell_at_index(i) +// cell_rect := shy.Rect{ +// x: cell.pos.x +// y: cell.pos.y +// width: g.config.cell_size.width +// height: g.config.cell_size.height +// } +// if cell_rect.contains(xy.x,xy.y) { +// return cell +// } +// } +// return none +// } + +// relative_position_in_cell_at returns the relative position of `xy` inside the cell located at `xy` coordinate. +// pub fn (g Grid2D) relative_position_in_cell_at(xy vec.Vec2[f32]) ?vec.Vec2[f32] { +// if cell := g.cell_at(xy) { +// pos := cell.pos +// return xy - pos +// } +// return none +// } + +// bookmark returns a `Bookmark` struct that can be used to restore the cells as they +// occur when this function is called. Restoring is done with `load_bookmark` +pub fn (g Grid2D) bookmark() Bookmark2D { + cell := g.origin + return Bookmark2D{ + config: g.config + ixy: cell.ixy + pos: cell.pos + } +} + +// load_bookmark instantly loads the bookmark. +// NOTE: bookmarks are not reliable across grids unless the grids has identical dimensions and cell sizes, +// use the `config` field of the `Bookmark2D` to access the data of the grid that the bookmark was saved in. +pub fn (mut g Grid2D) load_bookmark(bookmark Bookmark2D) { + ixy := bookmark.ixy + pos := bookmark.pos + + g.origin = Cell2D{ + grid_id: g.config.id + ixy: ixy + pos: pos + } +} + +// Utility functions + +// touch moves the grid a bit back and forth by the same amount to trigger any cells on the edges +fn (mut g Grid2D) touch() { + g.move(vec.Vec2[f32]{1, 1}) + g.move(vec.Vec2[f32]{-1, -1}) +} + +fn (mut g Grid2D) update_dimensions() { + g.update_fills() + + w := g.fill.width + h := g.fill.height + + g.cols = int(mth.ceil(w / g.config.cell_size.width)) + g.rows = int(mth.ceil(h / g.config.cell_size.height)) + + epln('${@STRUCT}.${@FN}: w: ${w}, h: ${h} g.cols: ${g.cols}, g.rows: ${g.rows}, cell: ${g.config.cell_size}') +} + +fn (mut g Grid2D) update_fills() { + vp := g.config.dimensions + cell_size := g.config.cell_size + fill_multiply := g.config.fill_multiply + fill_box := vec.Vec2{cell_size.width * fill_multiply.width, cell_size.height * fill_multiply.height} + bounds_box := vec.Vec2{(fill_box.x + cell_size.width) * 1.0, (fill_box.y + cell_size.height) * 1.0} + + rect := shy.Rect{0, 0, vp.width, vp.height} + fill_area := rect.grow(fill_box.x, fill_box.y, fill_box.x, fill_box.y) + bounds_area := rect.grow(bounds_box.x, bounds_box.y, bounds_box.x, bounds_box.y) + + g.fill = fill_area + g.bounds = bounds_area +}