Drawing!

main
Shad Amethyst 2 years ago
parent ec1660d006
commit f5aa5e86ff
Signed by: amethyst
GPG Key ID: D970C8DD1D6DEE36

@ -7,3 +7,7 @@ edition = "2021"
[dependencies]
dyn-clone = "1.0"
palette = "0.6"
[dev-dependencies]
colored = "2.0"

@ -24,6 +24,9 @@ use tile::*;
pub mod context;
use context::*;
pub mod text;
use text::*;
pub struct World {
panes: Vec<Pane>,
}
@ -32,6 +35,8 @@ pub mod prelude {
pub use crate::pane::Pane;
pub use crate::World;
pub use crate::text::{TextSurface, TextChar};
pub use crate::context::UpdateContext;
pub use crate::signal::Signal;
pub use crate::tile::Tile;

@ -157,4 +157,80 @@ impl Pane {
.enumerate()
.filter_map(move |(i, v)| Some((i % self.width, i / self.width, v)))
}
pub fn draw(&self, dx: isize, dy: isize, surface: &mut TextSurface) {
for (x, y, tile) in self.tiles() {
let x = x as isize + dx;
let y = y as isize + dy;
if x >= 0 && y >= 0 {
tile.draw(x as usize, y as usize, surface);
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_pane_draw() {
use crate::tile::Wire;
use Orientation::*;
let mut surface = TextSurface::new(3, 3);
let mut pane = test_tile_setup!(2, 2, [Wire::new(Horizontal), Wire::new(Vertical), Wire::new(Any), ()]);
test_set_signal!(pane, (0, 0), Direction::Right);
pane.draw(0, 0, &mut surface);
assert_eq!(surface.get(0, 0).unwrap().ch, '-');
assert_eq!(surface.get(1, 0).unwrap().ch, '|');
assert_eq!(surface.get(0, 1).unwrap().ch, '+');
assert_eq!(surface.get(1, 1), Some(TextChar::default()));
for n in 0..3 {
assert_eq!(surface.get(2, n), Some(TextChar::default()));
assert_eq!(surface.get(n, 2), Some(TextChar::default()));
}
// With offset (1, 0)
let mut surface = TextSurface::new(3, 3);
pane.draw(1, 0, &mut surface);
assert_eq!(surface.get(1, 0).unwrap().ch, '-');
assert_eq!(surface.get(2, 0).unwrap().ch, '|');
assert_eq!(surface.get(1, 1).unwrap().ch, '+');
assert_eq!(surface.get(2, 1), Some(TextChar::default()));
for n in 0..3 {
assert_eq!(surface.get(0, n), Some(TextChar::default()));
assert_eq!(surface.get(n, 2), Some(TextChar::default()));
}
// With offset (0, 1)
let mut surface = TextSurface::new(3, 3);
pane.draw(0, 1, &mut surface);
assert_eq!(surface.get(0, 1).unwrap().ch, '-');
assert_eq!(surface.get(1, 1).unwrap().ch, '|');
assert_eq!(surface.get(0, 2).unwrap().ch, '+');
assert_eq!(surface.get(1, 2), Some(TextChar::default()));
for n in 0..3 {
assert_eq!(surface.get(2, n), Some(TextChar::default()));
assert_eq!(surface.get(n, 0), Some(TextChar::default()));
}
// Draw outside of bounds with offset (2, 2)
let mut surface = TextSurface::new(3, 3);
pane.draw(2, 2, &mut surface);
assert_eq!(surface.get(2, 2).unwrap().ch, '-');
for y in 0..3 {
for x in 0..3 {
if (x, y) != (2, 2) {
assert_eq!(surface.get(x, y), Some(TextChar::default()));
}
}
}
}
}

@ -0,0 +1,153 @@
use super::*;
use palette::{FromColor, Srgb};
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct TextChar {
pub ch: char,
pub fg: Srgb<u8>,
pub bg: Option<Srgb<u8>>,
}
impl TextChar {
#[inline]
pub fn new<C1, C2>(ch: char, fg: C1, bg: Option<C2>) -> Self
where
Srgb<u8>: FromColor<C1>,
Srgb<u8>: FromColor<C2>,
{
Self {
ch,
fg: Srgb::from_color(fg),
bg: bg.map(|x| Srgb::from_color(x)),
}
}
#[inline]
pub fn from_char(ch: impl Into<char>) -> Self {
Self {
ch: ch.into(),
fg: Srgb::new(255, 255, 255),
bg: None,
}
}
#[inline]
pub fn from_state(ch: impl Into<char>, state: State) -> Self {
Self {
ch: ch.into(),
fg: match state {
State::Idle => Srgb::new(128, 128, 128),
State::Active => Srgb::new(220, 255, 255),
State::Dormant => Srgb::new(100, 60, 60),
},
bg: None
}
}
}
impl Default for TextChar {
fn default() -> Self {
Self {
ch: ' ',
fg: Srgb::new(255, 255, 255),
bg: None
}
}
}
pub struct TextSurface {
width: usize,
height: usize,
chars: Vec<TextChar>,
}
impl TextSurface {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
chars: vec![TextChar::default(); width * height],
}
}
/// Returns the [`TextChar`] at `(x, y)`, if it exists.
///
/// ## Example
///
/// ```
/// # use stackline::prelude::*;
/// let surface = TextSurface::new(1, 1);
///
/// assert_eq!(surface.get(0, 0), Some(TextChar::default()));
/// ```
pub fn get(&self, x: usize, y: usize) -> Option<TextChar> {
if self.in_bounds(x, y) {
Some(self.chars[y * self.width + x])
} else {
None
}
}
/// Sets the [`TextChar`] at `(x, y)` to `c`, if `(x, y)` is within the bounds of this `TextSurface`.
/// Returns `None` if `(x, y)` is outside of the bounds.
///
/// ## Example
///
/// ```
/// # use stackline::prelude::*;
/// let mut surface = TextSurface::new(2, 2);
///
/// surface.set(0, 0, TextChar::from_char('a')).unwrap();
/// assert_eq!(surface.get(0, 0,), Some(TextChar::from_char('a')));
/// ```
pub fn set(&mut self, x: usize, y: usize, c: TextChar) -> Option<()> {
if self.in_bounds(x, y) {
self.chars[y * self.width + x] = c;
Some(())
} else {
None
}
}
/// Returns `true` iff `(x, y)` is within the bounds of this `TextSurface`.
pub fn in_bounds(&self, x: usize, y: usize) -> bool {
x < self.width && y < self.height
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
// TODO: resize
}
#[cfg(test)]
impl std::fmt::Display for TextSurface {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use colored::Colorize;
for y in 0..self.height {
for x in 0..self.width {
let ch = self.chars[y * self.width + x];
let mut string = String::from(ch.ch)
.truecolor(ch.fg.red, ch.fg.green, ch.fg.blue);
if let Some(bg) = ch.bg {
string = string.on_truecolor(bg.red, bg.green, bg.blue);
}
write!(f, "{}", string)?;
}
write!(f, "\n")?;
}
Ok(())
}
}

@ -0,0 +1,117 @@
use super::*;
/** Represents a tile that may be empty and may have a signal. The tile may only have a signal if it isn't empty.
Cloning a `FullTile` results in a `FullTile` that does not have any signal.
## Invariants
- `self.cell.is_none() -> self.signal.is_none()`
- `self.accepts_signal() -> self.cell.is_some()`
**/
#[derive(Clone, Debug)]
pub struct FullTile {
cell: Option<AnyTile>,
signal: Option<Signal>,
state: State,
pub(crate) updated: bool,
}
// NOTE: should not implement Tile
impl FullTile {
pub fn new(cell: Option<AnyTile>) -> Self {
Self {
cell,
signal: None,
state: State::default(),
updated: false,
}
}
pub fn accepts_signal(&self, direction: Direction) -> bool {
match self.cell {
Some(ref tile) => self.state.accepts_signal() && tile.accepts_signal(direction),
None => false,
}
}
/// Returns `Some` iff self.cell.is_some()
pub(crate) fn set_signal(&mut self, signal: Option<Signal>) -> Option<()> {
if self.cell.is_some() {
self.signal = signal;
Some(())
} else {
None
}
}
/// Returns the internal state of this full tile
#[inline]
pub fn get<'b>(&'b self) -> Option<&'b AnyTile> {
self.cell.as_ref()
}
/// Returns a mutable reference to the internal state of this tile
#[inline]
pub fn get_mut<'b>(&'b mut self) -> Option<&'b mut AnyTile> {
self.cell.as_mut()
}
/// Returns the signal of this tile
#[inline]
pub fn signal<'b>(&'b self) -> Option<&'b Signal> {
self.signal.as_ref()
}
#[inline]
pub fn take_signal(&mut self) -> Option<Signal> {
std::mem::take(&mut self.signal)
}
#[inline]
pub fn state(&self) -> State {
self.state
}
#[inline]
pub fn set_state(&mut self, state: State) {
if self.cell.is_some() {
self.state = state
}
}
#[inline]
pub fn next_state(&mut self) {
self.state = self.state.next();
}
/// Draws itself on a [`TextSurface`] at `(x, y)`.
/// If the tile is empty, does nothing
pub fn draw(&self, x: usize, y: usize, surface: &mut TextSurface) {
match self.cell {
Some(ref cell) => cell.draw(x, y, self.state, surface),
None => {}
}
}
}
impl Default for FullTile {
#[inline]
fn default() -> Self {
Self::new(None)
}
}
impl<T: Tile + 'static> From<T> for FullTile {
#[inline]
fn from(tile: T) -> Self {
Self::new(Some(AnyTile::new(tile)))
}
}
impl From<()> for FullTile {
#[inline]
fn from(_empty: ()) -> Self {
Self::new(None)
}
}

@ -4,112 +4,8 @@ use dyn_clone::{clone_box, DynClone};
mod wire;
pub use wire::*;
/** Represents a tile that may be empty and may have a signal. The tile may only have a signal if it isn't empty.
Cloning a `FullTile` results in a `FullTile` that does not have any signal.
## Invariants
- `self.cell.is_none() -> self.signal.is_none()`
- `self.accepts_signal() -> self.cell.is_some()`
**/
#[derive(Clone, Debug)]
pub struct FullTile {
cell: Option<AnyTile>,
signal: Option<Signal>,
state: State,
pub(crate) updated: bool,
}
// NOTE: should not implement Tile
impl FullTile {
pub fn new(cell: Option<AnyTile>) -> Self {
Self {
cell,
signal: None,
state: State::default(),
updated: false,
}
}
pub fn accepts_signal(&self, direction: Direction) -> bool {
match self.cell {
Some(ref tile) => self.state.accepts_signal() && tile.accepts_signal(direction),
None => false,
}
}
/// Returns `Some` iff self.cell.is_some()
pub(crate) fn set_signal(&mut self, signal: Option<Signal>) -> Option<()> {
if self.cell.is_some() {
self.signal = signal;
Some(())
} else {
None
}
}
/// Returns the internal state of this full tile
#[inline]
pub fn get<'b>(&'b self) -> Option<&'b AnyTile> {
self.cell.as_ref()
}
/// Returns a mutable reference to the internal state of this tile
#[inline]
pub fn get_mut<'b>(&'b mut self) -> Option<&'b mut AnyTile> {
self.cell.as_mut()
}
/// Returns the signal of this tile
#[inline]
pub fn signal<'b>(&'b self) -> Option<&'b Signal> {
self.signal.as_ref()
}
#[inline]
pub fn take_signal(&mut self) -> Option<Signal> {
std::mem::take(&mut self.signal)
}
#[inline]
pub fn state(&self) -> State {
self.state
}
#[inline]
pub fn set_state(&mut self, state: State) {
if self.cell.is_some() {
self.state = state
}
}
#[inline]
pub fn next_state(&mut self) {
self.state = self.state.next();
}
}
impl Default for FullTile {
#[inline]
fn default() -> Self {
Self::new(None)
}
}
impl<T: Tile + 'static> From<T> for FullTile {
#[inline]
fn from(tile: T) -> Self {
Self::new(Some(AnyTile::new(tile)))
}
}
impl From<()> for FullTile {
#[inline]
fn from(_empty: ()) -> Self {
Self::new(None)
}
}
mod full;
pub use full::*;
pub trait Tile: DynClone + std::fmt::Debug {
/// Function to be called when the tile needs to be updated.
@ -124,6 +20,15 @@ pub trait Tile: DynClone + std::fmt::Debug {
fn accepts_signal(&self, direction: Direction) -> bool {
true
}
/// Should draw itself on a [`TextSurface`].
/// The `Tile` is allowed to draw outside of its coordinates, although doing so might cause glitches.
// TODO: Use a 2d slice type
#[inline]
#[allow(unused_variables)]
fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) {
// noop
}
}
#[derive(Debug)]
@ -144,6 +49,11 @@ impl AnyTile {
pub fn accepts_signal(&self, direction: Direction) -> bool {
self.0.accepts_signal(direction)
}
#[inline]
pub fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) {
self.0.draw(x, y, state, surface);
}
}
impl Clone for AnyTile {

@ -35,6 +35,16 @@ impl Tile for Wire {
fn accepts_signal(&self, direction: Direction) -> bool {
self.0.contains(direction)
}
fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) {
let ch = match self.0 {
Orientation::Horizontal => '-',
Orientation::Vertical => '|',
Orientation::Any => '+'
};
surface.set(x, y, TextChar::from_state(ch, state));
}
}
#[derive(Clone, Debug)]
@ -65,6 +75,17 @@ impl Tile for Diode {
context.next_state();
}
}
fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) {
let ch = match self.0 {
Direction::Up => '^',
Direction::Down => 'v',
Direction::Left => '<',
Direction::Right => '>',
};
surface.set(x, y, TextChar::from_state(ch, state));
}
}
#[derive(Clone, Debug)]
@ -101,6 +122,17 @@ impl Tile for Resistor {
}
}
}
fn draw(&self, x: usize, y: usize, state: State, surface: &mut TextSurface) {
let ch = match self.direction {
Direction::Up => '\u{219f}', // Upwards Two Headed Arrow
Direction::Down => '\u{21a1}', // Downwards Two Headed Arrow
Direction::Left => '\u{219e}', // Leftwards Two Headed Arrow
Direction::Right => '\u{21a0}', // Rightwards Two Headed Arrow
};
surface.set(x, y, TextChar::from_state(ch, state));
}
}
#[cfg(test)]

Loading…
Cancel
Save