Merge pull request #13 from cccb/fonts
More CP437, PrimitiveGrid::map, renamings
This commit is contained in:
commit
30d74ff07d
45 changed files with 2396 additions and 1557 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -147,9 +147,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.1.29"
|
version = "1.1.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1"
|
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -610,7 +610,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servicepoint"
|
name = "servicepoint"
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitvec",
|
"bitvec",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
|
|
@ -626,7 +626,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servicepoint_binding_c"
|
name = "servicepoint_binding_c"
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cbindgen",
|
"cbindgen",
|
||||||
"servicepoint",
|
"servicepoint",
|
||||||
|
|
@ -634,7 +634,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servicepoint_binding_cs"
|
name = "servicepoint_binding_cs"
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"csbindgen",
|
"csbindgen",
|
||||||
"servicepoint",
|
"servicepoint",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ members = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
missing-docs = "warn"
|
missing-docs = "warn"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ cargo add servicepoint
|
||||||
or
|
or
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
servicepoint = "0.9.1"
|
servicepoint = "0.10.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,26 @@ use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(short, long, default_value = "localhost:2342")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value = "localhost:2342",
|
||||||
|
help = "Address of the display"
|
||||||
|
)]
|
||||||
destination: String,
|
destination: String,
|
||||||
#[arg(short, long, num_args = 1.., value_delimiter = '\n')]
|
#[arg(short, long, num_args = 1.., value_delimiter = '\n',
|
||||||
|
help = "Text to send - specify multiple times for multiple lines")]
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
#[arg(short, long, default_value_t = true)]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = true,
|
||||||
|
help = "Clear screen before sending text"
|
||||||
|
)]
|
||||||
clear: bool,
|
clear: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// example: `cargo run -- --text "Hallo,
|
/// example: `cargo run -- --text "Hallo" --text "CCCB"`
|
||||||
/// CCCB"`
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut cli = Cli::parse();
|
let mut cli = Cli::parse();
|
||||||
if cli.text.is_empty() {
|
if cli.text.is_empty() {
|
||||||
|
|
@ -31,15 +41,11 @@ fn main() {
|
||||||
.expect("sending clear failed");
|
.expect("sending clear failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = cli.text.iter().fold(String::new(), move |str, line| {
|
let text = cli.text.join("\n");
|
||||||
let is_first = str.is_empty();
|
|
||||||
str + if is_first { "" } else { "\n" } + line
|
|
||||||
});
|
|
||||||
|
|
||||||
let grid = CharGrid::from(&*text);
|
let grid = CharGrid::from(&*text);
|
||||||
let cp437_grid = Cp437Grid::from(&grid);
|
let cp437_grid = Cp437Grid::from(&grid);
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send(Command::Cp437Data(Origin::new(0, 0), cp437_grid))
|
.send(Command::Cp437Data(Origin::ZERO, cp437_grid))
|
||||||
.expect("sending text failed");
|
.expect("sending text failed");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,24 @@ fn main() {
|
||||||
let connection = Connection::open(cli.destination)
|
let connection = Connection::open(cli.destination)
|
||||||
.expect("could not connect to display");
|
.expect("could not connect to display");
|
||||||
|
|
||||||
let mut pixels = PixelGrid::max_sized();
|
let mut pixels = Bitmap::max_sized();
|
||||||
pixels.fill(true);
|
pixels.fill(true);
|
||||||
|
|
||||||
let command = Command::BitmapLinearWin(
|
let command = Command::BitmapLinearWin(
|
||||||
Origin::new(0, 0),
|
Origin::ZERO,
|
||||||
pixels,
|
pixels,
|
||||||
CompressionCode::Uncompressed,
|
CompressionCode::Uncompressed,
|
||||||
);
|
);
|
||||||
connection.send(command).expect("send failed");
|
connection.send(command).expect("send failed");
|
||||||
|
|
||||||
let max_brightness = usize::from(u8::from(Brightness::MAX));
|
let max_brightness: u8 = Brightness::MAX.into();
|
||||||
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||||
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
|
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
|
||||||
*byte = Brightness::try_from((index % max_brightness) as u8).unwrap();
|
let level = index as u8 % max_brightness;
|
||||||
|
*byte = Brightness::try_from(level).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.send(Command::CharBrightness(Origin::new(0, 0), brightnesses))
|
.send(Command::CharBrightness(Origin::ZERO, brightnesses))
|
||||||
.expect("send failed");
|
.expect("send failed");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ fn main() {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let command = Command::BitmapLinearWin(
|
let command = Command::BitmapLinearWin(
|
||||||
Origin::new(0, 0),
|
Origin::ZERO,
|
||||||
field.clone(),
|
field.clone(),
|
||||||
CompressionCode::Lzma,
|
CompressionCode::Lzma,
|
||||||
);
|
);
|
||||||
|
|
@ -34,7 +34,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn iteration(field: PixelGrid) -> PixelGrid {
|
fn iteration(field: Bitmap) -> Bitmap {
|
||||||
let mut next = field.clone();
|
let mut next = field.clone();
|
||||||
for x in 0..field.width() {
|
for x in 0..field.width() {
|
||||||
for y in 0..field.height() {
|
for y in 0..field.height() {
|
||||||
|
|
@ -51,7 +51,7 @@ fn iteration(field: PixelGrid) -> PixelGrid {
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count_neighbors(field: &PixelGrid, x: i32, y: i32) -> i32 {
|
fn count_neighbors(field: &Bitmap, x: i32, y: i32) -> i32 {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for nx in x - 1..=x + 1 {
|
for nx in x - 1..=x + 1 {
|
||||||
for ny in y - 1..=y + 1 {
|
for ny in y - 1..=y + 1 {
|
||||||
|
|
@ -78,8 +78,8 @@ fn count_neighbors(field: &PixelGrid, x: i32, y: i32) -> i32 {
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_random_field(probability: f64) -> PixelGrid {
|
fn make_random_field(probability: f64) -> Bitmap {
|
||||||
let mut field = PixelGrid::max_sized();
|
let mut field = Bitmap::max_sized();
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let d = distributions::Bernoulli::new(probability).unwrap();
|
let d = distributions::Bernoulli::new(probability).unwrap();
|
||||||
for x in 0..field.width() {
|
for x in 0..field.width() {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ fn main() {
|
||||||
let connection = Connection::open(Cli::parse().destination)
|
let connection = Connection::open(Cli::parse().destination)
|
||||||
.expect("could not connect to display");
|
.expect("could not connect to display");
|
||||||
|
|
||||||
let mut pixels = PixelGrid::max_sized();
|
let mut pixels = Bitmap::max_sized();
|
||||||
for x_offset in 0..usize::MAX {
|
for x_offset in 0..usize::MAX {
|
||||||
pixels.fill(false);
|
pixels.fill(false);
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = Command::BitmapLinearWin(
|
let command = Command::BitmapLinearWin(
|
||||||
Origin::new(0, 0),
|
Origin::ZERO,
|
||||||
pixels.clone(),
|
pixels.clone(),
|
||||||
CompressionCode::Lzma,
|
CompressionCode::Lzma,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,11 @@ fn main() {
|
||||||
|
|
||||||
// put all pixels in on state
|
// put all pixels in on state
|
||||||
if cli.enable_all {
|
if cli.enable_all {
|
||||||
let mut filled_grid = PixelGrid::max_sized();
|
let mut filled_grid = Bitmap::max_sized();
|
||||||
filled_grid.fill(true);
|
filled_grid.fill(true);
|
||||||
|
|
||||||
let command = BitmapLinearWin(
|
let command = BitmapLinearWin(
|
||||||
Origin::new(0, 0),
|
Origin::ZERO,
|
||||||
filled_grid,
|
filled_grid,
|
||||||
CompressionCode::Lzma,
|
CompressionCode::Lzma,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Example for how to use the WebSocket connection
|
//! Example for how to use the WebSocket connection
|
||||||
|
|
||||||
use servicepoint::{
|
use servicepoint::{
|
||||||
Command, CompressionCode, Connection, Grid, Origin, PixelGrid,
|
Bitmap, Command, CompressionCode, Connection, Grid, Origin,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -13,7 +13,7 @@ fn main() {
|
||||||
// use send_mut instead of send
|
// use send_mut instead of send
|
||||||
connection.send_mut(Command::Clear).unwrap();
|
connection.send_mut(Command::Clear).unwrap();
|
||||||
|
|
||||||
let mut pixels = PixelGrid::max_sized();
|
let mut pixels = Bitmap::max_sized();
|
||||||
pixels.fill(true);
|
pixels.fill(true);
|
||||||
|
|
||||||
// use send_mut instead of send
|
// use send_mut instead of send
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ fn main() {
|
||||||
let connection = Connection::open(cli.destination)
|
let connection = Connection::open(cli.destination)
|
||||||
.expect("could not connect to display");
|
.expect("could not connect to display");
|
||||||
|
|
||||||
let mut enabled_pixels = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
let mut enabled_pixels = Bitmap::max_sized();
|
||||||
enabled_pixels.fill(true);
|
enabled_pixels.fill(true);
|
||||||
|
|
||||||
for x_offset in 0..PIXEL_WIDTH {
|
for x_offset in 0..PIXEL_WIDTH {
|
||||||
|
|
|
||||||
292
crates/servicepoint/src/bitmap.rs
Normal file
292
crates/servicepoint/src/bitmap.rs
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
use bitvec::order::Msb0;
|
||||||
|
use bitvec::prelude::BitSlice;
|
||||||
|
use bitvec::slice::IterMut;
|
||||||
|
|
||||||
|
use crate::{BitVec, DataRef, Grid, SpBitVec, PIXEL_HEIGHT, PIXEL_WIDTH};
|
||||||
|
|
||||||
|
/// A grid of pixels stored in packed bytes.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Bitmap {
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
bit_vec: SpBitVec,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
/// Creates a new [Bitmap] with the specified dimensions.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `width`: size in pixels in x-direction
|
||||||
|
/// - `height`: size in pixels in y-direction
|
||||||
|
///
|
||||||
|
/// returns: [Bitmap] initialized to all pixels off
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - when the width is not dividable by 8
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
assert_eq!(width % 8, 0);
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bit_vec: BitVec::repeat(false, width * height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new pixel grid with the size of the whole screen.
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_sized() -> Self {
|
||||||
|
Self::new(PIXEL_WIDTH, PIXEL_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a [Bitmap] with the specified dimensions from the provided data.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `width`: size in pixels in x-direction
|
||||||
|
/// - `height`: size in pixels in y-direction
|
||||||
|
///
|
||||||
|
/// returns: [Bitmap] that contains a copy of the provided data
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - when the dimensions and data size do not match exactly.
|
||||||
|
/// - when the width is not dividable by 8
|
||||||
|
#[must_use]
|
||||||
|
pub fn load(width: usize, height: usize, data: &[u8]) -> Self {
|
||||||
|
assert_eq!(width % 8, 0);
|
||||||
|
assert_eq!(data.len(), height * width / 8);
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bit_vec: BitVec::from_slice(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all cells in [Bitmap].
|
||||||
|
///
|
||||||
|
/// Order is equivalent to the following loop:
|
||||||
|
/// ```
|
||||||
|
/// # use servicepoint::{Bitmap, Grid};
|
||||||
|
/// # let grid = Bitmap::new(8,2);
|
||||||
|
/// for y in 0..grid.height() {
|
||||||
|
/// for x in 0..grid.width() {
|
||||||
|
/// grid.get(x, y);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &bool> {
|
||||||
|
self.bit_vec.iter().by_refs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all cells in [Bitmap] mutably.
|
||||||
|
///
|
||||||
|
/// Order is equivalent to the following loop:
|
||||||
|
/// ```
|
||||||
|
/// # use servicepoint::{Bitmap, Grid};
|
||||||
|
/// # let mut grid = Bitmap::new(8,2);
|
||||||
|
/// # let value = false;
|
||||||
|
/// for y in 0..grid.height() {
|
||||||
|
/// for x in 0..grid.width() {
|
||||||
|
/// grid.set(x, y, value);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// # use servicepoint::{Bitmap, Grid};
|
||||||
|
/// # let mut grid = Bitmap::new(8,2);
|
||||||
|
/// # let value = false;
|
||||||
|
/// for (index, mut pixel) in grid.iter_mut().enumerate() {
|
||||||
|
/// pixel.set(index % 2 == 0)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
|
||||||
|
self.bit_vec.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all rows in [Bitmap] top to bottom.
|
||||||
|
pub fn iter_rows(&self) -> IterRows {
|
||||||
|
IterRows {
|
||||||
|
bitmap: self,
|
||||||
|
row: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid<bool> for Bitmap {
|
||||||
|
/// Sets the value of the specified position in the [Bitmap].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `x` and `y`: position of the cell
|
||||||
|
/// - `value`: the value to write to the cell
|
||||||
|
///
|
||||||
|
/// returns: old value of the cell
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// When accessing `x` or `y` out of bounds.
|
||||||
|
fn set(&mut self, x: usize, y: usize, value: bool) {
|
||||||
|
self.assert_in_bounds(x, y);
|
||||||
|
self.bit_vec.set(x + y * self.width, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, x: usize, y: usize) -> bool {
|
||||||
|
self.assert_in_bounds(x, y);
|
||||||
|
self.bit_vec[x + y * self.width]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the state of all pixels in the [Bitmap].
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `this`: instance to write to
|
||||||
|
/// - `value`: the value to set all pixels to
|
||||||
|
fn fill(&mut self, value: bool) {
|
||||||
|
self.bit_vec.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn width(&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> usize {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataRef<u8> for Bitmap {
|
||||||
|
fn data_ref_mut(&mut self) -> &mut [u8] {
|
||||||
|
self.bit_vec.as_raw_mut_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_ref(&self) -> &[u8] {
|
||||||
|
self.bit_vec.as_raw_slice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bitmap> for Vec<u8> {
|
||||||
|
/// Turns a [Bitmap] into the underlying [`Vec<u8>`].
|
||||||
|
fn from(value: Bitmap) -> Self {
|
||||||
|
value.bit_vec.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IterRows<'t> {
|
||||||
|
bitmap: &'t Bitmap,
|
||||||
|
row: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> Iterator for IterRows<'t> {
|
||||||
|
type Item = &'t BitSlice<u8, Msb0>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.row >= self.bitmap.height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = self.row * self.bitmap.width;
|
||||||
|
let end = start + self.bitmap.width;
|
||||||
|
self.row += 1;
|
||||||
|
Some(&self.bitmap.bit_vec[start..end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Bitmap, DataRef, Grid};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fill() {
|
||||||
|
let mut grid = Bitmap::new(8, 2);
|
||||||
|
assert_eq!(grid.data_ref(), [0x00, 0x00]);
|
||||||
|
|
||||||
|
grid.fill(true);
|
||||||
|
assert_eq!(grid.data_ref(), [0xFF, 0xFF]);
|
||||||
|
|
||||||
|
grid.fill(false);
|
||||||
|
assert_eq!(grid.data_ref(), [0x00, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_set() {
|
||||||
|
let mut grid = Bitmap::new(8, 2);
|
||||||
|
assert!(!grid.get(0, 0));
|
||||||
|
assert!(!grid.get(1, 1));
|
||||||
|
|
||||||
|
grid.set(5, 0, true);
|
||||||
|
grid.set(1, 1, true);
|
||||||
|
assert_eq!(grid.data_ref(), [0x04, 0x40]);
|
||||||
|
|
||||||
|
assert!(grid.get(5, 0));
|
||||||
|
assert!(grid.get(1, 1));
|
||||||
|
assert!(!grid.get(1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load() {
|
||||||
|
let mut grid = Bitmap::new(8, 3);
|
||||||
|
for x in 0..grid.width {
|
||||||
|
for y in 0..grid.height {
|
||||||
|
grid.set(x, y, (x + y) % 2 == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
|
||||||
|
|
||||||
|
let data: Vec<u8> = grid.into();
|
||||||
|
|
||||||
|
let grid = Bitmap::load(8, 3, &data);
|
||||||
|
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn out_of_bounds_x() {
|
||||||
|
let vec = Bitmap::new(8, 2);
|
||||||
|
vec.get(8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn out_of_bounds_y() {
|
||||||
|
let mut vec = Bitmap::new(8, 2);
|
||||||
|
vec.set(1, 2, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter() {
|
||||||
|
let grid = Bitmap::new(8, 2);
|
||||||
|
assert_eq!(16, grid.iter().count())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_rows() {
|
||||||
|
let grid = Bitmap::load(8, 2, &[0x04, 0x40]);
|
||||||
|
let mut iter = grid.iter_rows();
|
||||||
|
|
||||||
|
assert_eq!(iter.next().unwrap().count_ones(), 1);
|
||||||
|
assert_eq!(iter.next().unwrap().count_ones(), 1);
|
||||||
|
assert_eq!(None, iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_mut() {
|
||||||
|
let mut grid = Bitmap::new(8, 2);
|
||||||
|
for (index, mut pixel) in grid.iter_mut().enumerate() {
|
||||||
|
pixel.set(index % 2 == 0);
|
||||||
|
}
|
||||||
|
assert_eq!(grid.data_ref(), [0xAA, 0xAA]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn data_ref_mut() {
|
||||||
|
let mut grid = Bitmap::new(8, 2);
|
||||||
|
let data = grid.data_ref_mut();
|
||||||
|
data[1] = 0x0F;
|
||||||
|
assert!(grid.get(7, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,17 @@ impl Brightness {
|
||||||
pub const MAX: Brightness = Brightness(11);
|
pub const MAX: Brightness = Brightness(11);
|
||||||
/// lowest possible brightness value, 0
|
/// lowest possible brightness value, 0
|
||||||
pub const MIN: Brightness = Brightness(0);
|
pub const MIN: Brightness = Brightness(0);
|
||||||
|
|
||||||
|
/// Create a brightness value without returning an error for brightnesses above [Brightness::MAX].
|
||||||
|
///
|
||||||
|
/// returns: the specified value as a [Brightness], or [Brightness::MAX].
|
||||||
|
pub fn saturating_from(value: u8) -> Brightness {
|
||||||
|
if value > Brightness::MAX.into() {
|
||||||
|
Brightness::MAX
|
||||||
|
} else {
|
||||||
|
Brightness(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Brightness {
|
impl Default for Brightness {
|
||||||
|
|
@ -138,4 +149,10 @@ mod tests {
|
||||||
let actual = PrimitiveGrid::from(&grid);
|
let actual = PrimitiveGrid::from(&grid);
|
||||||
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
|
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn saturating_convert() {
|
||||||
|
assert_eq!(Brightness::MAX, Brightness::saturating_from(100));
|
||||||
|
assert_eq!(Brightness(5), Brightness::saturating_from(5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
command_code::CommandCode,
|
command_code::CommandCode,
|
||||||
compression::into_decompressed,
|
compression::into_decompressed,
|
||||||
packet::{Header, Packet},
|
packet::{Header, Packet},
|
||||||
Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin, PixelGrid,
|
Bitmap, Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin,
|
||||||
Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE,
|
Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -76,12 +76,7 @@ pub enum Command {
|
||||||
|
|
||||||
/// Show text on the screen.
|
/// Show text on the screen.
|
||||||
///
|
///
|
||||||
/// The text is sent in the form of a 2D grid of characters.
|
/// The text is sent in the form of a 2D grid of [CP-437] encoded characters.
|
||||||
///
|
|
||||||
/// <div class="warning">
|
|
||||||
/// The library does not currently convert between UTF-8 and CP-437.
|
|
||||||
/// Because Rust expects UTF-8 strings, it might be necessary to only send ASCII for now.
|
|
||||||
/// </div>
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
|
@ -100,6 +95,7 @@ pub enum Command {
|
||||||
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
|
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
|
||||||
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
|
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
|
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
|
||||||
Cp437Data(Origin<Tiles>, Cp437Grid),
|
Cp437Data(Origin<Tiles>, Cp437Grid),
|
||||||
|
|
||||||
/// Overwrites a rectangular region of pixels.
|
/// Overwrites a rectangular region of pixels.
|
||||||
|
|
@ -109,23 +105,23 @@ pub enum Command {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
/// # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
|
||||||
/// # let connection = servicepoint::Connection::Fake;
|
/// # let connection = servicepoint::Connection::Fake;
|
||||||
/// #
|
/// #
|
||||||
/// let mut pixels = PixelGrid::max_sized();
|
/// let mut pixels = Bitmap::max_sized();
|
||||||
/// // draw something to the pixels here
|
/// // draw something to the pixels here
|
||||||
/// # pixels.set(2, 5, true);
|
/// # pixels.set(2, 5, true);
|
||||||
///
|
///
|
||||||
/// // create command to send pixels
|
/// // create command to send pixels
|
||||||
/// let command = Command::BitmapLinearWin(
|
/// let command = Command::BitmapLinearWin(
|
||||||
/// servicepoint::Origin::new(0, 0),
|
/// servicepoint::Origin::ZERO,
|
||||||
/// pixels,
|
/// pixels,
|
||||||
/// CompressionCode::Uncompressed
|
/// CompressionCode::Uncompressed
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
/// connection.send(command).expect("send failed");
|
/// connection.send(command).expect("send failed");
|
||||||
/// ```
|
/// ```
|
||||||
BitmapLinearWin(Origin<Pixels>, PixelGrid, CompressionCode),
|
BitmapLinearWin(Origin<Pixels>, Bitmap, CompressionCode),
|
||||||
|
|
||||||
/// Set the brightness of all tiles to the same value.
|
/// Set the brightness of all tiles to the same value.
|
||||||
///
|
///
|
||||||
|
|
@ -217,9 +213,8 @@ pub enum Command {
|
||||||
BitmapLegacy,
|
BitmapLegacy,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// Err values for [Command::try_from].
|
/// Err values for [Command::try_from].
|
||||||
#[derive(PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum TryFromPacketError {
|
pub enum TryFromPacketError {
|
||||||
/// the contained command code does not correspond to a known command
|
/// the contained command code does not correspond to a known command
|
||||||
InvalidCommand(u16),
|
InvalidCommand(u16),
|
||||||
|
|
@ -342,7 +337,7 @@ impl Command {
|
||||||
|
|
||||||
Ok(Command::BitmapLinearWin(
|
Ok(Command::BitmapLinearWin(
|
||||||
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
|
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
|
||||||
PixelGrid::load(
|
Bitmap::load(
|
||||||
tile_w as usize * TILE_SIZE,
|
tile_w as usize * TILE_SIZE,
|
||||||
pixel_h as usize,
|
pixel_h as usize,
|
||||||
&payload,
|
&payload,
|
||||||
|
|
@ -376,7 +371,7 @@ impl Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method for Packets into `BitMapLinear*`-Commands
|
/// Helper method for Packets into `BitmapLinear*`-Commands
|
||||||
fn packet_into_linear_bitmap(
|
fn packet_into_linear_bitmap(
|
||||||
packet: Packet,
|
packet: Packet,
|
||||||
) -> Result<(SpBitVec, CompressionCode), TryFromPacketError> {
|
) -> Result<(SpBitVec, CompressionCode), TryFromPacketError> {
|
||||||
|
|
@ -500,7 +495,8 @@ mod tests {
|
||||||
command_code::CommandCode,
|
command_code::CommandCode,
|
||||||
origin::Pixels,
|
origin::Pixels,
|
||||||
packet::{Header, Packet},
|
packet::{Header, Packet},
|
||||||
Brightness, Command, CompressionCode, Origin, PixelGrid, PrimitiveGrid,
|
Bitmap, Brightness, BrightnessGrid, Command, CompressionCode, Origin,
|
||||||
|
PrimitiveGrid,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn round_trip(original: Command) {
|
fn round_trip(original: Command) {
|
||||||
|
|
@ -592,8 +588,8 @@ mod tests {
|
||||||
compression,
|
compression,
|
||||||
));
|
));
|
||||||
round_trip(Command::BitmapLinearWin(
|
round_trip(Command::BitmapLinearWin(
|
||||||
Origin::new(0, 0),
|
Origin::ZERO,
|
||||||
PixelGrid::max_sized(),
|
Bitmap::max_sized(),
|
||||||
compression,
|
compression,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -718,7 +714,7 @@ mod tests {
|
||||||
for compression in all_compressions().to_owned() {
|
for compression in all_compressions().to_owned() {
|
||||||
let p: Packet = Command::BitmapLinearWin(
|
let p: Packet = Command::BitmapLinearWin(
|
||||||
Origin::new(16, 8),
|
Origin::new(16, 8),
|
||||||
PixelGrid::new(8, 8),
|
Bitmap::new(8, 8),
|
||||||
compression,
|
compression,
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
@ -907,4 +903,28 @@ mod tests {
|
||||||
Origin::new(1, 0) + Origin::new(3, 2)
|
Origin::new(1, 0) + Origin::new(3, 2)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packet_into_char_brightness_invalid() {
|
||||||
|
let grid = BrightnessGrid::new(2, 2);
|
||||||
|
let command = Command::CharBrightness(Origin::ZERO, grid);
|
||||||
|
let mut packet: Packet = command.into();
|
||||||
|
let slot = packet.payload.get_mut(1).unwrap();
|
||||||
|
*slot = 23;
|
||||||
|
assert_eq!(
|
||||||
|
Command::try_from(packet),
|
||||||
|
Err(TryFromPacketError::InvalidBrightness(23))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packet_into_brightness_invalid() {
|
||||||
|
let mut packet: Packet = Command::Brightness(Brightness::MAX).into();
|
||||||
|
let slot = packet.payload.get_mut(0).unwrap();
|
||||||
|
*slot = 42;
|
||||||
|
assert_eq!(
|
||||||
|
Command::try_from(packet),
|
||||||
|
Err(TryFromPacketError::InvalidBrightness(42))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use servicepoint::{Command, CompressionCode, Origin, PixelGrid};
|
/// # use servicepoint::{Command, CompressionCode, Origin, Bitmap};
|
||||||
/// // create command without payload compression
|
/// // create command without payload compression
|
||||||
/// # let pixels = PixelGrid::max_sized();
|
/// # let pixels = Bitmap::max_sized();
|
||||||
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Uncompressed);
|
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Uncompressed);
|
||||||
///
|
///
|
||||||
/// // create command with payload compressed with lzma and appropriate header flags
|
/// // create command with payload compressed with lzma and appropriate header flags
|
||||||
/// # let pixels = PixelGrid::max_sized();
|
/// # let pixels = Bitmap::max_sized();
|
||||||
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Lzma);
|
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Lzma);
|
||||||
/// ```
|
/// ```
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::cp437::Cp437LoadError::InvalidChar;
|
//! Conversion between UTF-8 and CP-437.
|
||||||
|
//!
|
||||||
|
//! Most of the functionality is only available with feature "cp437" enabled.
|
||||||
|
|
||||||
use crate::{Grid, PrimitiveGrid};
|
use crate::{Grid, PrimitiveGrid};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
@ -10,9 +13,16 @@ pub type Cp437Grid = PrimitiveGrid<u8>;
|
||||||
/// A grid containing UTF-8 characters.
|
/// A grid containing UTF-8 characters.
|
||||||
pub type CharGrid = PrimitiveGrid<char>;
|
pub type CharGrid = PrimitiveGrid<char>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Errors that can occur when loading CP-437.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Cp437LoadError {
|
pub enum Cp437LoadError {
|
||||||
InvalidChar { index: usize, char: char },
|
/// Invalid character in input prevented loading
|
||||||
|
InvalidChar {
|
||||||
|
/// invalid character is at this position in input
|
||||||
|
index: usize,
|
||||||
|
/// the invalid character
|
||||||
|
char: char,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cp437Grid {
|
impl Cp437Grid {
|
||||||
|
|
@ -36,7 +46,7 @@ impl Cp437Grid {
|
||||||
|
|
||||||
for (index, char) in value.chars().enumerate() {
|
for (index, char) in value.chars().enumerate() {
|
||||||
if !char.is_ascii() {
|
if !char.is_ascii() {
|
||||||
return Err(InvalidChar { index, char });
|
return Err(Cp437LoadError::InvalidChar { index, char });
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_lf = char == '\n';
|
let is_lf = char == '\n';
|
||||||
|
|
@ -85,21 +95,15 @@ mod feature_cp437 {
|
||||||
|
|
||||||
/// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
|
/// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
|
||||||
///
|
///
|
||||||
/// Mostly follows CP437, except for:
|
/// Mostly follows CP437, except 0x0A, which is kept for use as line ending.
|
||||||
/// * 0x0A & 0x0D are kept for use as line endings.
|
|
||||||
/// * 0x1A is used for SAUCE.
|
|
||||||
/// * 0x1B is used for ANSI escape sequences.
|
|
||||||
///
|
|
||||||
/// These exclusions should be fine since most programs can't even use them
|
|
||||||
/// without issues. And this makes rendering simpler too.
|
|
||||||
///
|
///
|
||||||
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
|
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
|
||||||
///
|
///
|
||||||
/// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0
|
/// Mostly copied from https://github.com/kip93/cp437-tools. License: GPL-3.0
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const CP437_TO_UTF8: [char; 256] = [
|
pub const CP437_TO_UTF8: [char; 256] = [
|
||||||
/* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼',
|
/* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '♪', '♫', '☼',
|
||||||
/* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔', '▲', '▼',
|
/* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼',
|
||||||
/* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/',
|
/* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/',
|
||||||
/* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
/* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
||||||
/* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
/* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||||
|
|
@ -116,7 +120,7 @@ mod feature_cp437 {
|
||||||
/* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ',
|
/* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ',
|
||||||
];
|
];
|
||||||
|
|
||||||
const UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
|
static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
|
||||||
once_cell::sync::Lazy::new(|| {
|
once_cell::sync::Lazy::new(|| {
|
||||||
let pairs = CP437_TO_UTF8
|
let pairs = CP437_TO_UTF8
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -125,48 +129,34 @@ mod feature_cp437 {
|
||||||
HashMap::from_iter(pairs)
|
HashMap::from_iter(pairs)
|
||||||
});
|
});
|
||||||
|
|
||||||
const MISSING_CHAR_CP437: u8 = 0x3F;
|
const MISSING_CHAR_CP437: u8 = 0x3F; // '?'
|
||||||
|
|
||||||
impl From<&Cp437Grid> for CharGrid {
|
impl From<&Cp437Grid> for CharGrid {
|
||||||
fn from(value: &Cp437Grid) -> Self {
|
fn from(value: &Cp437Grid) -> Self {
|
||||||
let mut grid = Self::new(value.width(), value.height());
|
value.map(cp437_to_char)
|
||||||
|
|
||||||
for y in 0..grid.height() {
|
|
||||||
for x in 0..grid.width() {
|
|
||||||
let converted = CP437_TO_UTF8[value.get(x, y) as usize];
|
|
||||||
grid.set(x, y, converted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
grid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&CharGrid> for Cp437Grid {
|
impl From<&CharGrid> for Cp437Grid {
|
||||||
fn from(value: &CharGrid) -> Self {
|
fn from(value: &CharGrid) -> Self {
|
||||||
let mut grid = Self::new(value.width(), value.height());
|
value.map(char_to_cp437)
|
||||||
|
|
||||||
for y in 0..grid.height() {
|
|
||||||
for x in 0..grid.width() {
|
|
||||||
let char = value.get(x, y);
|
|
||||||
let converted = *UTF8_TO_CP437
|
|
||||||
.get(&char)
|
|
||||||
.unwrap_or(&MISSING_CHAR_CP437);
|
|
||||||
grid.set(x, y, converted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
grid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for CharGrid {
|
impl From<&str> for CharGrid {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &str) -> Self {
|
||||||
let value = value.replace("\r\n", "\n");
|
let value = value.replace("\r\n", "\n");
|
||||||
let lines = value.split('\n').collect::<Vec<_>>();
|
let mut lines = value
|
||||||
|
.split('\n')
|
||||||
|
.map(move |line| line.trim_end())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let width =
|
let width =
|
||||||
lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len()));
|
lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len()));
|
||||||
|
|
||||||
|
while lines.last().is_some_and(move |line| line.is_empty()) {
|
||||||
|
_ = lines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
let mut grid = Self::new(width, lines.len());
|
let mut grid = Self::new(width, lines.len());
|
||||||
for (y, line) in lines.iter().enumerate() {
|
for (y, line) in lines.iter().enumerate() {
|
||||||
for (x, char) in line.chars().enumerate() {
|
for (x, char) in line.chars().enumerate() {
|
||||||
|
|
@ -177,6 +167,44 @@ mod feature_cp437 {
|
||||||
grid
|
grid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&CharGrid> for String {
|
||||||
|
fn from(value: &CharGrid) -> Self {
|
||||||
|
value
|
||||||
|
.iter_rows()
|
||||||
|
.map(move |chars| {
|
||||||
|
chars
|
||||||
|
.collect::<String>()
|
||||||
|
.replace('\0', " ")
|
||||||
|
.trim_end()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the provided bytes to UTF-8.
|
||||||
|
pub fn cp437_to_str(cp437: &[u8]) -> String {
|
||||||
|
cp437.iter().map(move |char| cp437_to_char(*char)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a single CP-437 character to UTF-8.
|
||||||
|
pub fn cp437_to_char(cp437: u8) -> char {
|
||||||
|
CP437_TO_UTF8[cp437 as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the provided text to CP-437 bytes.
|
||||||
|
///
|
||||||
|
/// Characters that are not available are mapped to '?'.
|
||||||
|
pub fn str_to_cp437(utf8: &str) -> Vec<u8> {
|
||||||
|
utf8.chars().map(char_to_cp437).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a single UTF-8 character to CP-437.
|
||||||
|
pub fn char_to_cp437(utf8: char) -> u8 {
|
||||||
|
*UTF8_TO_CP437.get(&utf8).unwrap_or(&MISSING_CHAR_CP437)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -204,12 +232,23 @@ mod tests {
|
||||||
// line break will be added
|
// line break will be added
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_ascii_invalid() {
|
||||||
|
assert_eq!(
|
||||||
|
Err(Cp437LoadError::InvalidChar {
|
||||||
|
char: '🥶',
|
||||||
|
index: 2
|
||||||
|
}),
|
||||||
|
Cp437Grid::load_ascii("?#🥶42", 3, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "cp437")]
|
#[cfg(feature = "cp437")]
|
||||||
mod tests_feature_cp437 {
|
mod tests_feature_cp437 {
|
||||||
use crate::{CharGrid, Cp437Grid};
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trip_cp437() {
|
fn round_trip_cp437() {
|
||||||
|
|
@ -218,4 +257,48 @@ mod tests_feature_cp437 {
|
||||||
let actual = CharGrid::from(&cp437);
|
let actual = CharGrid::from(&cp437);
|
||||||
assert_eq!(actual, utf8);
|
assert_eq!(actual, utf8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_str() {
|
||||||
|
// test text from https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios
|
||||||
|
let utf8 = r#"A quick brown fox jumps over the lazy dog.
|
||||||
|
0123456789 ¿?¡!`'"., <>()[]{} &@%*^#$\/
|
||||||
|
|
||||||
|
* Wieniläinen sioux'ta puhuva ökyzombie diggaa Åsan roquefort-tacoja.
|
||||||
|
* Ça me fait peur de fêter noël là, sur cette île bizarroïde où une mère et sa môme essaient de me tuer avec un gâteau à la cigüe brûlé.
|
||||||
|
* Zwölf Boxkämpfer jagten Eva quer über den Sylter Deich.
|
||||||
|
* El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.
|
||||||
|
|
||||||
|
┌─┬─┐ ╔═╦═╗ ╒═╤═╕ ╓─╥─╖
|
||||||
|
│ │ │ ║ ║ ║ │ │ │ ║ ║ ║
|
||||||
|
├─┼─┤ ╠═╬═╣ ╞═╪═╡ ╟─╫─╢
|
||||||
|
└─┴─┘ ╚═╩═╝ ╘═╧═╛ ╙─╨─╜
|
||||||
|
|
||||||
|
░░░░░ ▐▀█▀▌ .·∙•○°○•∙·.
|
||||||
|
▒▒▒▒▒ ▐ █ ▌ ☺☻ ♥♦♣♠ ♪♫☼
|
||||||
|
▓▓▓▓▓ ▐▀█▀▌ $ ¢ £ ¥ ₧
|
||||||
|
█████ ▐▄█▄▌ ◄►▲▼ ←→↑↓↕↨
|
||||||
|
|
||||||
|
⌠
|
||||||
|
│dx ≡ Σ √x²ⁿ·δx
|
||||||
|
⌡"#;
|
||||||
|
|
||||||
|
let cp437 = str_to_cp437(utf8);
|
||||||
|
let actual = cp437_to_str(&*cp437);
|
||||||
|
assert_eq!(utf8, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_invalid() {
|
||||||
|
assert_eq!(cp437_to_char(char_to_cp437('😜')), '?');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn str_to_char_grid() {
|
||||||
|
let original = "Hello\r\nWorld!\n...\n";
|
||||||
|
let grid = CharGrid::from(original);
|
||||||
|
assert_eq!(3, grid.height());
|
||||||
|
let actual = String::from(&grid);
|
||||||
|
assert_eq!("Hello\nWorld!\n...", actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,15 +76,9 @@ pub trait Grid<T> {
|
||||||
///
|
///
|
||||||
/// When the specified position is out of bounds for this grid.
|
/// When the specified position is out of bounds for this grid.
|
||||||
fn assert_in_bounds(&self, x: usize, y: usize) {
|
fn assert_in_bounds(&self, x: usize, y: usize) {
|
||||||
assert!(
|
let width = self.width();
|
||||||
x < self.width(),
|
assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})");
|
||||||
"cannot access index [{x}, {y}] because x is outside of bounds 0..{}",
|
let height = self.height();
|
||||||
self.width() - 1
|
assert!(y < height, "cannot access index [{x}, {y}] because x is outside of bounds [0..{height})");
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
y < self.height(),
|
|
||||||
"cannot access index [{x}, {y}] because y is outside of bounds 0..{}",
|
|
||||||
self.height() - 1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
//! use servicepoint::{Command, CompressionCode, Grid, Bitmap};
|
||||||
//!
|
//!
|
||||||
//! let connection = servicepoint::Connection::open("127.0.0.1:2342")
|
//! let connection = servicepoint::Connection::open("127.0.0.1:2342")
|
||||||
//! .expect("connection failed");
|
//! .expect("connection failed");
|
||||||
|
|
@ -18,15 +18,15 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
|
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
|
||||||
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
|
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
|
||||||
//! // turn on all pixels in a grid
|
//! // turn on all pixels in a grid
|
||||||
//! let mut pixels = PixelGrid::max_sized();
|
//! let mut pixels = Bitmap::max_sized();
|
||||||
//! pixels.fill(true);
|
//! pixels.fill(true);
|
||||||
//!
|
//!
|
||||||
//! // create command to send pixels
|
//! // create command to send pixels
|
||||||
//! let command = Command::BitmapLinearWin(
|
//! let command = Command::BitmapLinearWin(
|
||||||
//! servicepoint::Origin::new(0, 0),
|
//! servicepoint::Origin::ZERO,
|
||||||
//! pixels,
|
//! pixels,
|
||||||
//! CompressionCode::Uncompressed
|
//! CompressionCode::Uncompressed
|
||||||
//! );
|
//! );
|
||||||
|
|
@ -40,6 +40,7 @@ use std::time::Duration;
|
||||||
pub use bitvec;
|
pub use bitvec;
|
||||||
use bitvec::prelude::{BitVec, Msb0};
|
use bitvec::prelude::{BitVec, Msb0};
|
||||||
|
|
||||||
|
pub use crate::bitmap::Bitmap;
|
||||||
pub use crate::brightness::{Brightness, BrightnessGrid};
|
pub use crate::brightness::{Brightness, BrightnessGrid};
|
||||||
pub use crate::command::{Command, Offset};
|
pub use crate::command::{Command, Offset};
|
||||||
pub use crate::compression_code::CompressionCode;
|
pub use crate::compression_code::CompressionCode;
|
||||||
|
|
@ -48,23 +49,22 @@ pub use crate::cp437::{CharGrid, Cp437Grid};
|
||||||
pub use crate::data_ref::DataRef;
|
pub use crate::data_ref::DataRef;
|
||||||
pub use crate::grid::Grid;
|
pub use crate::grid::Grid;
|
||||||
pub use crate::origin::{Origin, Pixels, Tiles};
|
pub use crate::origin::{Origin, Pixels, Tiles};
|
||||||
pub use crate::pixel_grid::PixelGrid;
|
|
||||||
pub use crate::primitive_grid::PrimitiveGrid;
|
pub use crate::primitive_grid::PrimitiveGrid;
|
||||||
|
|
||||||
type SpBitVec = BitVec<u8, Msb0>;
|
type SpBitVec = BitVec<u8, Msb0>;
|
||||||
|
|
||||||
|
mod bitmap;
|
||||||
mod brightness;
|
mod brightness;
|
||||||
mod command;
|
mod command;
|
||||||
mod command_code;
|
mod command_code;
|
||||||
mod compression;
|
mod compression;
|
||||||
mod compression_code;
|
mod compression_code;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod cp437;
|
pub mod cp437;
|
||||||
mod data_ref;
|
mod data_ref;
|
||||||
mod grid;
|
mod grid;
|
||||||
mod origin;
|
mod origin;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
mod pixel_grid;
|
|
||||||
mod primitive_grid;
|
mod primitive_grid;
|
||||||
|
|
||||||
/// size of a single tile in one dimension
|
/// size of a single tile in one dimension
|
||||||
|
|
@ -95,8 +95,8 @@ pub const TILE_HEIGHT: usize = 20;
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
|
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
|
||||||
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
/// let grid = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||||
/// ```
|
/// ```
|
||||||
pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
|
pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
|
||||||
|
|
||||||
|
|
@ -105,8 +105,8 @@ pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
|
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
|
||||||
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
/// let grid = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||||
/// ```
|
/// ```
|
||||||
pub const PIXEL_HEIGHT: usize = TILE_HEIGHT * TILE_SIZE;
|
pub const PIXEL_HEIGHT: usize = TILE_HEIGHT * TILE_SIZE;
|
||||||
|
|
||||||
|
|
@ -119,10 +119,10 @@ pub const PIXEL_COUNT: usize = PIXEL_WIDTH * PIXEL_HEIGHT;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use std::time::Instant;
|
/// # use std::time::Instant;
|
||||||
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, PixelGrid};
|
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, Bitmap};
|
||||||
/// # let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
/// # let connection = servicepoint::Connection::open("172.23.42.29:2342")
|
||||||
/// # .expect("connection failed");
|
/// # .expect("connection failed");
|
||||||
/// # let pixels = PixelGrid::max_sized();
|
/// # let pixels = Bitmap::max_sized();
|
||||||
/// loop {
|
/// loop {
|
||||||
/// let start = Instant::now();
|
/// let start = Instant::now();
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub struct Origin<Unit: DisplayUnit> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Unit: DisplayUnit> Origin<Unit> {
|
impl<Unit: DisplayUnit> Origin<Unit> {
|
||||||
/// Top-left. Equivalent to `Origin::new(0, 0)`.
|
/// Top-left. Equivalent to `Origin::ZERO`.
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ use std::mem::size_of;
|
||||||
|
|
||||||
use crate::compression::into_compressed;
|
use crate::compression::into_compressed;
|
||||||
use crate::{
|
use crate::{
|
||||||
command_code::CommandCode, Command, CompressionCode, Grid, Offset, Origin,
|
command_code::CommandCode, Bitmap, Command, CompressionCode, Grid, Offset,
|
||||||
PixelGrid, Pixels, Tiles, TILE_SIZE,
|
Origin, Pixels, Tiles, TILE_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A raw header.
|
/// A raw header.
|
||||||
|
|
@ -214,7 +214,7 @@ impl From<Command> for Packet {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Packet {
|
impl Packet {
|
||||||
/// Helper method for `BitMapLinear*`-Commands into [Packet]
|
/// Helper method for `BitmapLinear*`-Commands into [Packet]
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
fn bitmap_linear_into_packet(
|
fn bitmap_linear_into_packet(
|
||||||
command: CommandCode,
|
command: CommandCode,
|
||||||
|
|
@ -239,7 +239,7 @@ impl Packet {
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
fn bitmap_win_into_packet(
|
fn bitmap_win_into_packet(
|
||||||
origin: Origin<Pixels>,
|
origin: Origin<Pixels>,
|
||||||
pixels: PixelGrid,
|
pixels: Bitmap,
|
||||||
compression: CompressionCode,
|
compression: CompressionCode,
|
||||||
) -> Packet {
|
) -> Packet {
|
||||||
debug_assert_eq!(origin.x % 8, 0);
|
debug_assert_eq!(origin.x % 8, 0);
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,34 @@ impl<T: PrimitiveGridType> PrimitiveGrid<T> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert between PrimitiveGrid types.
|
||||||
|
///
|
||||||
|
/// See also [Iterator::map].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command].
|
||||||
|
/// ```
|
||||||
|
/// # fn foo(grid: &mut PrimitiveGrid<u8>) {}
|
||||||
|
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Origin, PrimitiveGrid, TILE_HEIGHT, TILE_WIDTH};
|
||||||
|
/// let mut grid: PrimitiveGrid<u8> = PrimitiveGrid::new(TILE_WIDTH, TILE_HEIGHT);
|
||||||
|
/// foo(&mut grid);
|
||||||
|
/// let grid: BrightnessGrid = grid.map(Brightness::saturating_from);
|
||||||
|
/// let command = Command::CharBrightness(Origin::ZERO, grid);
|
||||||
|
/// ```
|
||||||
|
pub fn map<TConverted, F>(&self, f: F) -> PrimitiveGrid<TConverted>
|
||||||
|
where
|
||||||
|
TConverted: PrimitiveGridType,
|
||||||
|
F: Fn(T) -> TConverted,
|
||||||
|
{
|
||||||
|
let data = self
|
||||||
|
.data_ref()
|
||||||
|
.iter()
|
||||||
|
.map(|elem| f(*elem))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
PrimitiveGrid::load(self.width(), self.height(), &data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PrimitiveGridType> Grid<T> for PrimitiveGrid<T> {
|
impl<T: PrimitiveGridType> Grid<T> for PrimitiveGrid<T> {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue