So much progress on b30
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
373e54c55e
commit
3dc320d524
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -904,6 +904,17 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "freetype-rs"
|
||||||
|
version = "0.36.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"freetype-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freetype-sys"
|
name = "freetype-sys"
|
||||||
version = "0.20.1"
|
version = "0.20.1"
|
||||||
|
@ -2672,6 +2683,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"edit-distance",
|
"edit-distance",
|
||||||
|
"freetype-rs",
|
||||||
"image 0.25.1",
|
"image 0.25.1",
|
||||||
"kd-tree",
|
"kd-tree",
|
||||||
"num",
|
"num",
|
||||||
|
|
|
@ -6,10 +6,11 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
edit-distance = "2.1.0"
|
edit-distance = "2.1.0"
|
||||||
|
freetype-rs = "0.36.0"
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
kd-tree = { version="0.6.0", features=["serde"] }
|
kd-tree = { version="0.6.0", features=["serde"] }
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c" }
|
plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] }
|
||||||
poise = "0.6.1"
|
poise = "0.6.1"
|
||||||
postcard = { version="1.0.8", features=["use-std"] }
|
postcard = { version="1.0.8", features=["use-std"] }
|
||||||
serde = "1.0.204"
|
serde = "1.0.204"
|
||||||
|
@ -19,5 +20,5 @@ tesseract = "0.15.1"
|
||||||
tokio = {version="1.38.0", features=["rt-multi-thread"]}
|
tokio = {version="1.38.0", features=["rt-multi-thread"]}
|
||||||
typenum = "1.17.0"
|
typenum = "1.17.0"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
BIN
data/assets/count_background.png
Normal file
BIN
data/assets/count_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
data/assets/diff-byd.png
Normal file
BIN
data/assets/diff-byd.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
BIN
data/assets/diff-etr.png
Normal file
BIN
data/assets/diff-etr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
BIN
data/assets/diff-ftr.png
Normal file
BIN
data/assets/diff-ftr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
BIN
data/assets/diff-prs.png
Normal file
BIN
data/assets/diff-prs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
data/assets/diff-pst.png
Normal file
BIN
data/assets/diff-pst.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
411
src/bitmap.rs
Normal file
411
src/bitmap.rs
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
use freetype::{
|
||||||
|
face::{KerningMode, LoadFlag},
|
||||||
|
ffi::FT_GLYPH_BBOX_PIXELS,
|
||||||
|
Face,
|
||||||
|
};
|
||||||
|
use num::traits::Euclid;
|
||||||
|
|
||||||
|
use crate::context::Error;
|
||||||
|
|
||||||
|
// {{{ BitmapCanvas
|
||||||
|
pub struct BitmapCanvas {
|
||||||
|
pub buffer: Box<[u8]>,
|
||||||
|
pub width: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitmapCanvas {
|
||||||
|
// {{{ Draw pixel
|
||||||
|
pub fn set_pixel(&mut self, pos: (u32, u32), color: (u8, u8, u8, u8)) {
|
||||||
|
let index = 3 * (pos.1 * self.width + pos.0) as usize;
|
||||||
|
let alpha = color.3 as u32;
|
||||||
|
self.buffer[index + 0] =
|
||||||
|
((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8;
|
||||||
|
self.buffer[index + 1] =
|
||||||
|
((alpha * color.1 as u32 + (255 - alpha) * self.buffer[index + 1] as u32) / 255) as u8;
|
||||||
|
self.buffer[index + 2] =
|
||||||
|
((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8;
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw RBG image
|
||||||
|
/// Draws a bitmap image
|
||||||
|
pub fn blit_rbg(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) {
|
||||||
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
|
for dx in 0..iw {
|
||||||
|
for dy in 0..ih {
|
||||||
|
let x = pos.0 + dx as i32;
|
||||||
|
let y = pos.1 + dy as i32;
|
||||||
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
let r = src[(dx + dy * iw) as usize * 3];
|
||||||
|
let g = src[(dx + dy * iw) as usize * 3 + 1];
|
||||||
|
let b = src[(dx + dy * iw) as usize * 3 + 2];
|
||||||
|
|
||||||
|
let color = (r, g, b, 255);
|
||||||
|
|
||||||
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw RGBA image
|
||||||
|
/// Draws a bitmap image taking care of the alpha channel.
|
||||||
|
pub fn blit_rbga(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), src: &[u8]) {
|
||||||
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
|
for dx in 0..iw {
|
||||||
|
for dy in 0..ih {
|
||||||
|
let x = pos.0 + dx as i32;
|
||||||
|
let y = pos.1 + dy as i32;
|
||||||
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
let r = src[(dx + dy * iw) as usize * 4];
|
||||||
|
let g = src[(dx + dy * iw) as usize * 4 + 1];
|
||||||
|
let b = src[(dx + dy * iw) as usize * 4 + 2];
|
||||||
|
let a = src[(dx + dy * iw) as usize * 4 + 3];
|
||||||
|
|
||||||
|
let color = (r, g, b, a);
|
||||||
|
|
||||||
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Fill
|
||||||
|
/// Fill with solid color
|
||||||
|
pub fn fill(&mut self, pos: (i32, i32), (iw, ih): (u32, u32), color: (u8, u8, u8, u8)) {
|
||||||
|
let height = self.buffer.len() as u32 / 3 / self.width;
|
||||||
|
for dx in 0..iw {
|
||||||
|
for dy in 0..ih {
|
||||||
|
let x = pos.0 + dx as i32;
|
||||||
|
let y = pos.1 + dy as i32;
|
||||||
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
self.set_pixel((x as u32, y as u32), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw text
|
||||||
|
/// Render text
|
||||||
|
pub fn text(
|
||||||
|
&mut self,
|
||||||
|
pos: (i32, i32),
|
||||||
|
face: Face,
|
||||||
|
size: u32,
|
||||||
|
text: &str,
|
||||||
|
color: (u8, u8, u8, u8),
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
face.set_char_size(0, (size as isize) << 6, 300, 300)?;
|
||||||
|
|
||||||
|
let mut pen_x = 0;
|
||||||
|
let kerning = face.has_kerning();
|
||||||
|
let mut previous = None;
|
||||||
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
for c in text.chars() {
|
||||||
|
let glyph_index = face
|
||||||
|
.get_char_index(c as usize)
|
||||||
|
.ok_or_else(|| format!("Could not get glyph index for char {:?}", c))?;
|
||||||
|
|
||||||
|
if let Some(previous) = previous
|
||||||
|
&& kerning
|
||||||
|
{
|
||||||
|
let delta = face.get_kerning(previous, glyph_index, KerningMode::KerningDefault)?;
|
||||||
|
pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy
|
||||||
|
}
|
||||||
|
|
||||||
|
face.load_glyph(glyph_index, LoadFlag::DEFAULT)?;
|
||||||
|
|
||||||
|
data.push((pen_x, face.glyph().get_glyph()?));
|
||||||
|
pen_x += face.glyph().advance().x >> 6;
|
||||||
|
previous = Some(glyph_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut x_min = 32000;
|
||||||
|
let mut y_min = 32000;
|
||||||
|
let mut x_max = -32000;
|
||||||
|
let mut y_max = -32000;
|
||||||
|
|
||||||
|
for (pen_x, glyph) in &data {
|
||||||
|
let mut bbox = glyph.get_cbox(FT_GLYPH_BBOX_PIXELS);
|
||||||
|
|
||||||
|
bbox.xMin += pen_x;
|
||||||
|
bbox.xMax += pen_x;
|
||||||
|
|
||||||
|
if bbox.xMin < x_min {
|
||||||
|
x_min = bbox.xMin
|
||||||
|
}
|
||||||
|
|
||||||
|
if bbox.xMax < x_max {
|
||||||
|
x_max = bbox.xMax
|
||||||
|
}
|
||||||
|
|
||||||
|
if bbox.yMin < y_min {
|
||||||
|
y_min = bbox.yMin
|
||||||
|
}
|
||||||
|
|
||||||
|
if bbox.yMax < y_max {
|
||||||
|
y_max = bbox.yMax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we really grew the string bbox
|
||||||
|
if x_min > x_max {
|
||||||
|
x_min = 0;
|
||||||
|
x_max = 0;
|
||||||
|
y_min = 0;
|
||||||
|
y_max = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pos_x, glyph) in &data {
|
||||||
|
let b_glyph = glyph.to_bitmap(freetype::RenderMode::Normal, None)?;
|
||||||
|
let bitmap = b_glyph.bitmap();
|
||||||
|
let pixel_mode = bitmap.pixel_mode()?;
|
||||||
|
println!(
|
||||||
|
"Pixel mode: {:?}, width {:?}, height {:?}, len {:?}, pen x {:?}",
|
||||||
|
pixel_mode,
|
||||||
|
bitmap.width(),
|
||||||
|
bitmap.rows(),
|
||||||
|
bitmap.buffer().len(),
|
||||||
|
pos_x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn new(width: u32, height: u32) -> Self {
|
||||||
|
let buffer = vec![u8::MAX; 8 * 3 * (width * height) as usize].into_boxed_slice();
|
||||||
|
Self { buffer, width }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Layout types
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LayoutBox {
|
||||||
|
relative_to: Option<(LayoutBoxId, i32, i32)>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct LayoutBoxId(usize);
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct LayoutManager {
|
||||||
|
boxes: Vec<LayoutBox>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LayoutDrawer {
|
||||||
|
pub layout: LayoutManager,
|
||||||
|
pub canvas: BitmapCanvas,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutManager {
|
||||||
|
// {{{ Trivial box creation
|
||||||
|
pub fn make_box(&mut self, width: u32, height: u32) -> LayoutBoxId {
|
||||||
|
let id = self.boxes.len();
|
||||||
|
self.boxes.push(LayoutBox {
|
||||||
|
relative_to: None,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
LayoutBoxId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_relative_box(
|
||||||
|
&mut self,
|
||||||
|
to: LayoutBoxId,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> LayoutBoxId {
|
||||||
|
let id = self.make_box(width, height);
|
||||||
|
self.edit_to_relative(id, to, x, y);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Chage box to be relative
|
||||||
|
pub fn edit_to_relative(
|
||||||
|
&mut self,
|
||||||
|
id: LayoutBoxId,
|
||||||
|
id_relative_to: LayoutBoxId,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
) {
|
||||||
|
let current = self.boxes[id.0];
|
||||||
|
let to = self.boxes[id_relative_to.0];
|
||||||
|
if let Some((current_points_to, dx, dy)) = current.relative_to
|
||||||
|
&& current_points_to != id_relative_to
|
||||||
|
{
|
||||||
|
self.edit_to_relative(current_points_to, id_relative_to, x - dx, y - dy);
|
||||||
|
} else {
|
||||||
|
self.boxes[id.0].relative_to = Some((id_relative_to, x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let a = self.lookup(id);
|
||||||
|
let b = self.lookup(id_relative_to);
|
||||||
|
assert_eq!((a.0 - b.0, a.1 - b.1), (x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Margins
|
||||||
|
#[inline]
|
||||||
|
pub fn margin(&mut self, id: LayoutBoxId, t: i32, r: i32, b: i32, l: i32) -> LayoutBoxId {
|
||||||
|
let inner = self.boxes[id.0];
|
||||||
|
let out = self.make_box(
|
||||||
|
(inner.width as i32 + l + r) as u32,
|
||||||
|
(inner.height as i32 + t + b) as u32,
|
||||||
|
);
|
||||||
|
self.edit_to_relative(id, out, l, t);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn margin_xy(&mut self, inner: LayoutBoxId, x: i32, y: i32) -> LayoutBoxId {
|
||||||
|
self.margin(inner, y, x, y, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn margin_uniform(&mut self, inner: LayoutBoxId, amount: i32) -> LayoutBoxId {
|
||||||
|
self.margin(inner, amount, amount, amount, amount)
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Glueing
|
||||||
|
#[inline]
|
||||||
|
pub fn glue_horizontally(
|
||||||
|
&mut self,
|
||||||
|
first_id: LayoutBoxId,
|
||||||
|
second_id: LayoutBoxId,
|
||||||
|
) -> LayoutBoxId {
|
||||||
|
let first = self.boxes[first_id.0];
|
||||||
|
let second = self.boxes[second_id.0];
|
||||||
|
let id = self.make_box(first.width.max(second.width), first.height + second.height);
|
||||||
|
|
||||||
|
self.edit_to_relative(first_id, id, 0, 0);
|
||||||
|
self.edit_to_relative(second_id, id, 0, first.height as i32);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn glue_vertically(
|
||||||
|
&mut self,
|
||||||
|
first_id: LayoutBoxId,
|
||||||
|
second_id: LayoutBoxId,
|
||||||
|
) -> LayoutBoxId {
|
||||||
|
let first = self.boxes[first_id.0];
|
||||||
|
let second = self.boxes[second_id.0];
|
||||||
|
let id = self.make_box(first.width + second.width, first.height.max(second.height));
|
||||||
|
|
||||||
|
self.edit_to_relative(first_id, id, 0, 0);
|
||||||
|
self.edit_to_relative(second_id, id, first.width as i32, 0);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Repeating
|
||||||
|
pub fn repeated_evenly(
|
||||||
|
&mut self,
|
||||||
|
id: LayoutBoxId,
|
||||||
|
amount: (u32, u32),
|
||||||
|
) -> (LayoutBoxId, impl Iterator<Item = (i32, i32)>) {
|
||||||
|
let inner = self.boxes[id.0];
|
||||||
|
let outer_id = self.make_box(inner.width * amount.0, inner.height * amount.1);
|
||||||
|
self.edit_to_relative(id, outer_id, 0, 0);
|
||||||
|
|
||||||
|
(
|
||||||
|
outer_id,
|
||||||
|
(0..amount.0 * amount.1).into_iter().map(move |i| {
|
||||||
|
let (y, x) = i.div_rem_euclid(&amount.0);
|
||||||
|
((x * inner.width) as i32, (y * inner.height) as i32)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Lookup box
|
||||||
|
pub fn lookup(&self, id: LayoutBoxId) -> (i32, i32, u32, u32) {
|
||||||
|
let current = self.boxes[id.0];
|
||||||
|
if let Some((to, dx, dy)) = current.relative_to {
|
||||||
|
let (x, y, _, _) = self.lookup(to);
|
||||||
|
(x + dx, y + dy, current.width, current.height)
|
||||||
|
} else {
|
||||||
|
(0, 0, current.width, current.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn width(&self, id: LayoutBoxId) -> u32 {
|
||||||
|
self.boxes[id.0].width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn height(&self, id: LayoutBoxId) -> u32 {
|
||||||
|
self.boxes[id.0].height
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn position_relative_to(&self, id: LayoutBoxId, pos: (i32, i32)) -> (i32, i32) {
|
||||||
|
let current = self.lookup(id);
|
||||||
|
((pos.0 as i32 + current.0), (pos.1 as i32 + current.1))
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutDrawer {
|
||||||
|
pub fn new(layout: LayoutManager, canvas: BitmapCanvas) -> Self {
|
||||||
|
Self { layout, canvas }
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{{ Drawing
|
||||||
|
// {{{ Draw pixel
|
||||||
|
pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: (u8, u8, u8, u8)) {
|
||||||
|
let pos = self
|
||||||
|
.layout
|
||||||
|
.position_relative_to(id, (pos.0 as i32, pos.1 as i32));
|
||||||
|
self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw RGB image
|
||||||
|
/// Draws a bitmap image
|
||||||
|
pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) {
|
||||||
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
|
self.canvas.blit_rbg(pos, dims, src);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw RGBA image
|
||||||
|
/// Draws a bitmap image taking care of the alpha channel.
|
||||||
|
pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: (i32, i32), dims: (u32, u32), src: &[u8]) {
|
||||||
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
|
self.canvas.blit_rbga(pos, dims, src);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Fill
|
||||||
|
/// Fills with solid color
|
||||||
|
pub fn fill(&mut self, id: LayoutBoxId, color: (u8, u8, u8, u8)) {
|
||||||
|
let current = self.layout.lookup(id);
|
||||||
|
self.canvas
|
||||||
|
.fill((current.0, current.1), (current.2, current.3), color);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Draw text
|
||||||
|
/// Render text
|
||||||
|
pub fn text(
|
||||||
|
&mut self,
|
||||||
|
id: LayoutBoxId,
|
||||||
|
pos: (i32, i32),
|
||||||
|
face: Face,
|
||||||
|
size: u32,
|
||||||
|
text: &str,
|
||||||
|
color: (u8, u8, u8, u8),
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let pos = self.layout.position_relative_to(id, pos);
|
||||||
|
self.canvas.text(pos, face, size, text, color)
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
// }}}
|
15
src/chart.rs
15
src/chart.rs
|
@ -1,7 +1,8 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use image::{ImageBuffer, Rgb};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{prelude::FromRow, SqlitePool};
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::context::Error;
|
use crate::context::Error;
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ impl TryFrom<String> for Side {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Song
|
// {{{ Song
|
||||||
#[derive(Debug, Clone, FromRow)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -91,7 +92,13 @@ pub struct Song {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Chart
|
// {{{ Chart
|
||||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Jacket {
|
||||||
|
pub raw: &'static [u8],
|
||||||
|
pub bitmap: &'static ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Chart {
|
pub struct Chart {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub song_id: u32,
|
pub song_id: u32,
|
||||||
|
@ -104,7 +111,7 @@ pub struct Chart {
|
||||||
pub note_count: u32,
|
pub note_count: u32,
|
||||||
pub chart_constant: u32,
|
pub chart_constant: u32,
|
||||||
|
|
||||||
pub cached_jacket: Option<&'static [u8]>,
|
pub cached_jacket: Option<Jacket>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chart {
|
impl Chart {
|
||||||
|
|
|
@ -19,8 +19,8 @@ pub async fn chart(
|
||||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||||
|
|
||||||
let attachement_name = "chart.png";
|
let attachement_name = "chart.png";
|
||||||
let icon_attachement = match chart.cached_jacket {
|
let icon_attachement = match chart.cached_jacket.as_ref() {
|
||||||
Some(bytes) => Some(CreateAttachment::bytes(bytes, attachement_name)),
|
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, attachement_name)),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,11 @@ use poise::{
|
||||||
use sqlx::query_as;
|
use sqlx::query_as;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bitmap::{BitmapCanvas, LayoutDrawer, LayoutManager},
|
||||||
|
chart::{Chart, Song},
|
||||||
context::{Context, Error},
|
context::{Context, Error},
|
||||||
score::{guess_song_and_chart, DbPlay, Score},
|
jacket::BITMAP_IMAGE_SIZE,
|
||||||
|
score::{guess_song_and_chart, DbPlay, Play, Score},
|
||||||
user::{discord_it_to_discord_user, User},
|
user::{discord_it_to_discord_user, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,7 +30,7 @@ use crate::{
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
subcommands("chart"),
|
subcommands("chart", "b30"),
|
||||||
subcommand_required
|
subcommand_required
|
||||||
)]
|
)]
|
||||||
pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
@ -212,7 +215,6 @@ pub async fn plot(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())),
|
.map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
root.present()?;
|
root.present()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,3 +230,226 @@ pub async fn plot(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ B30
|
||||||
|
/// Show the 30 best scores
|
||||||
|
#[poise::command(prefix_command, slash_command)]
|
||||||
|
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
let user = match User::from_context(&ctx).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => {
|
||||||
|
ctx.say("You are not an user in my database, sorry!")
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let plays: Vec<DbPlay> = query_as(
|
||||||
|
"
|
||||||
|
SELECT id, chart_id, user_id,
|
||||||
|
created_at, MAX(score) as score, zeta_score,
|
||||||
|
creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id
|
||||||
|
FROM plays p
|
||||||
|
WHERE user_id = ?
|
||||||
|
GROUP BY chart_id
|
||||||
|
ORDER BY score DESC
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(user.id)
|
||||||
|
.fetch_all(&ctx.data().db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if plays.len() < 30 {
|
||||||
|
ctx.reply("Not enough plays found").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consider not reallocating everything here
|
||||||
|
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
||||||
|
.into_iter()
|
||||||
|
.map(|play| {
|
||||||
|
let play = play.to_play();
|
||||||
|
// TODO: change the .lookup to perform binary search or something
|
||||||
|
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
|
||||||
|
Ok((play, song, chart))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
|
||||||
|
plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant));
|
||||||
|
plays.truncate(30);
|
||||||
|
|
||||||
|
let mut layout = LayoutManager::default();
|
||||||
|
let jacket_area = layout.make_box(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE);
|
||||||
|
let jacket_margin = 10;
|
||||||
|
let jacket_with_margin =
|
||||||
|
layout.margin(jacket_area, jacket_margin, jacket_margin, 5, jacket_margin);
|
||||||
|
let top_left_area = layout.make_box(90, layout.height(jacket_with_margin));
|
||||||
|
let top_area = layout.glue_vertically(top_left_area, jacket_with_margin);
|
||||||
|
let bottom_area = layout.make_box(layout.width(top_area), 40);
|
||||||
|
let item_area = layout.glue_horizontally(top_area, bottom_area);
|
||||||
|
let item_with_margin = layout.margin_xy(item_area, 25, 20);
|
||||||
|
let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6));
|
||||||
|
let root = item_grid;
|
||||||
|
|
||||||
|
// layout.normalize(root);
|
||||||
|
let width = layout.width(root);
|
||||||
|
let height = layout.height(root);
|
||||||
|
|
||||||
|
let canvas = BitmapCanvas::new(width, height);
|
||||||
|
let mut drawer = LayoutDrawer::new(layout, canvas);
|
||||||
|
|
||||||
|
let asset_cache = &ctx.data().jacket_cache;
|
||||||
|
let bg = &asset_cache.b30_background;
|
||||||
|
|
||||||
|
drawer.blit_rbg(
|
||||||
|
root,
|
||||||
|
(
|
||||||
|
-((bg.width() - width) as i32) / 2,
|
||||||
|
-((bg.height() - height) as i32) / 2,
|
||||||
|
),
|
||||||
|
bg.dimensions(),
|
||||||
|
bg.as_raw(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, origin) in item_origins.enumerate() {
|
||||||
|
drawer
|
||||||
|
.layout
|
||||||
|
.edit_to_relative(item_with_margin, item_grid, origin.0, origin.1);
|
||||||
|
|
||||||
|
drawer.fill(top_area, (59, 78, 102, 255));
|
||||||
|
|
||||||
|
let (_play, song, chart) = &plays[i];
|
||||||
|
|
||||||
|
// {{{ Display jacket
|
||||||
|
let jacket = chart.cached_jacket.as_ref().ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Cannot find jacket for chart {} [{:?}]",
|
||||||
|
song.title, chart.difficulty
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
drawer.blit_rbg(
|
||||||
|
jacket_area,
|
||||||
|
(0, 0),
|
||||||
|
jacket.bitmap.dimensions(),
|
||||||
|
&jacket.bitmap.as_raw(),
|
||||||
|
);
|
||||||
|
// }}}
|
||||||
|
// {{{ Display difficulty background
|
||||||
|
let diff_bg = &asset_cache.diff_backgrounds[chart.difficulty.to_index()];
|
||||||
|
drawer.blit_rbga(
|
||||||
|
jacket_area,
|
||||||
|
(
|
||||||
|
BITMAP_IMAGE_SIZE as i32 - (diff_bg.width() as i32) / 2,
|
||||||
|
-(diff_bg.height() as i32) / 2,
|
||||||
|
),
|
||||||
|
diff_bg.dimensions(),
|
||||||
|
&diff_bg.as_raw(),
|
||||||
|
);
|
||||||
|
// }}}
|
||||||
|
// {{{ Display difficulty text
|
||||||
|
let x_offset = if chart.level.ends_with("+") {
|
||||||
|
3
|
||||||
|
} else if chart.level == "11" {
|
||||||
|
-2
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
// jacket_area.draw_text(
|
||||||
|
// &chart.level,
|
||||||
|
// &TextStyle::from(("Exo", 30).into_font())
|
||||||
|
// .color(&WHITE)
|
||||||
|
// .with_anchor::<RGBAColor>(Pos {
|
||||||
|
// h_pos: HPos::Center,
|
||||||
|
// v_pos: VPos::Center,
|
||||||
|
// })
|
||||||
|
// .into_text_style(&jacket_area),
|
||||||
|
// (BITMAP_IMAGE_SIZE as i32 + x_offset, 2),
|
||||||
|
// )?;
|
||||||
|
// }}}
|
||||||
|
// {{{ Display chart name
|
||||||
|
// Draw background
|
||||||
|
drawer.fill(bottom_area, (0x82, 0x71, 0xA7, 255));
|
||||||
|
|
||||||
|
let tx = 10;
|
||||||
|
let ty = drawer.layout.height(bottom_area) as i32 / 2;
|
||||||
|
|
||||||
|
// let text = &song.title;
|
||||||
|
// let mut size = 30;
|
||||||
|
// let mut text_style = TextStyle::from(("Exo", size).into_font().style(FontStyle::Bold))
|
||||||
|
// .with_anchor::<RGBAColor>(Pos {
|
||||||
|
// h_pos: HPos::Left,
|
||||||
|
// v_pos: VPos::Center,
|
||||||
|
// })
|
||||||
|
// .into_text_style(&bottom_area);
|
||||||
|
//
|
||||||
|
// while text_style.font.layout_box(text).unwrap().1 .0 >= item_area.0 as i32 - 20 {
|
||||||
|
// size -= 3;
|
||||||
|
// text_style.font = ("Exo", size).into_font();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Draw drop shadow
|
||||||
|
// bottom_area.draw_text(
|
||||||
|
// &song.title,
|
||||||
|
// &text_style.color(&RGBAColor(0, 0, 0, 0.2)),
|
||||||
|
// (tx + 3, ty + 3),
|
||||||
|
// )?;
|
||||||
|
// bottom_area.draw_text(
|
||||||
|
// &song.title,
|
||||||
|
// &text_style.color(&RGBAColor(0, 0, 0, 0.2)),
|
||||||
|
// (tx - 3, ty + 3),
|
||||||
|
// )?;
|
||||||
|
// bottom_area.draw_text(
|
||||||
|
// &song.title,
|
||||||
|
// &text_style.color(&RGBAColor(0, 0, 0, 0.2)),
|
||||||
|
// (tx + 3, ty - 3),
|
||||||
|
// )?;
|
||||||
|
// bottom_area.draw_text(
|
||||||
|
// &song.title,
|
||||||
|
// &text_style.color(&RGBAColor(0, 0, 0, 0.2)),
|
||||||
|
// (tx - 3, ty - 3),
|
||||||
|
// )?;
|
||||||
|
|
||||||
|
// Draw text
|
||||||
|
// bottom_area.draw_text(&song.title, &text_style.color(&WHITE), (tx, ty))?;
|
||||||
|
// }}}
|
||||||
|
// {{{ Display index
|
||||||
|
let bg = &asset_cache.count_background;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
|
||||||
|
|
||||||
|
// let text_style = TextStyle::from(("Exo", 30).into_font().style(FontStyle::Bold))
|
||||||
|
// .with_anchor::<RGBAColor>(Pos {
|
||||||
|
// h_pos: HPos::Left,
|
||||||
|
// v_pos: VPos::Center,
|
||||||
|
// })
|
||||||
|
// .into_text_style(&area);
|
||||||
|
|
||||||
|
let tx = 7;
|
||||||
|
let ty = (jacket_margin + bg.height() as i32 / 2) - 3;
|
||||||
|
|
||||||
|
// Draw drop shadow
|
||||||
|
// area.draw_text(
|
||||||
|
// &format!("#{}", i + 1),
|
||||||
|
// &text_style.color(&BLACK),
|
||||||
|
// (tx + 2, ty + 2),
|
||||||
|
// )?;
|
||||||
|
|
||||||
|
// Draw main text
|
||||||
|
// area.draw_text(&format!("#{}", i + 1), &text_style.color(&WHITE), (tx, ty))?;
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out_buffer = Vec::new();
|
||||||
|
let image: ImageBuffer<Rgb<u8>, _> =
|
||||||
|
ImageBuffer::from_raw(width, height, drawer.canvas.buffer).unwrap();
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(&mut out_buffer);
|
||||||
|
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||||
|
|
||||||
|
let reply = CreateReply::default().attachment(CreateAttachment::bytes(out_buffer, "b30.png"));
|
||||||
|
ctx.send(reply).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::{fs, path::PathBuf, str::FromStr};
|
use std::{fs, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use image::{GenericImageView, Rgba};
|
use freetype::{Face, Library};
|
||||||
|
use image::{imageops::FilterType, GenericImageView, ImageBuffer, Rgb, Rgba};
|
||||||
use kd_tree::{KdMap, KdPoint};
|
use kd_tree::{KdMap, KdPoint};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::serde_as;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chart::{Difficulty, SongCache},
|
bitmap::BitmapCanvas,
|
||||||
|
chart::{Difficulty, Jacket, SongCache},
|
||||||
context::Error,
|
context::Error,
|
||||||
score::guess_chart_name,
|
score::guess_chart_name,
|
||||||
};
|
};
|
||||||
|
@ -15,11 +15,10 @@ use crate::{
|
||||||
/// How many sub-segments to split each side into
|
/// How many sub-segments to split each side into
|
||||||
pub const SPLIT_FACTOR: u32 = 8;
|
pub const SPLIT_FACTOR: u32 = 8;
|
||||||
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize;
|
||||||
|
pub const BITMAP_IMAGE_SIZE: u32 = 192;
|
||||||
|
|
||||||
#[serde_as]
|
#[derive(Debug, Clone)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ImageVec {
|
pub struct ImageVec {
|
||||||
#[serde_as(as = "[_; IMAGE_VEC_DIM]")]
|
|
||||||
pub colors: [f32; IMAGE_VEC_DIM],
|
pub colors: [f32; IMAGE_VEC_DIM],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,16 +43,16 @@ impl ImageVec {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
for (_, _, pixel) in cropped.pixels() {
|
for (_, _, pixel) in cropped.pixels() {
|
||||||
r += pixel.0[0] as u64;
|
r += (pixel.0[0] as u64).pow(2);
|
||||||
g += pixel.0[1] as u64;
|
g += (pixel.0[1] as u64).pow(2);
|
||||||
b += pixel.0[2] as u64;
|
b += (pixel.0[2] as u64).pow(2);
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = count as f64;
|
let count = count as f64;
|
||||||
let r = r as f64 / count;
|
let r = (r as f64 / count).sqrt();
|
||||||
let g = g as f64 / count;
|
let g = (g as f64 / count).sqrt();
|
||||||
let b = b as f64 / count;
|
let b = (b as f64 / count).sqrt();
|
||||||
colors[i as usize * 3 + 0] = r as f32;
|
colors[i as usize * 3 + 0] = r as f32;
|
||||||
colors[i as usize * 3 + 1] = g as f32;
|
colors[i as usize * 3 + 1] = g as f32;
|
||||||
colors[i as usize * 3 + 2] = b as f32;
|
colors[i as usize * 3 + 2] = b as f32;
|
||||||
|
@ -77,9 +76,11 @@ impl KdPoint for ImageVec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct JacketCache {
|
pub struct JacketCache {
|
||||||
tree: KdMap<ImageVec, u32>,
|
tree: KdMap<ImageVec, u32>,
|
||||||
|
pub b30_background: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||||
|
pub count_background: ImageBuffer<Rgba<u8>, Vec<u8>>,
|
||||||
|
pub diff_backgrounds: [ImageBuffer<Rgba<u8>, Vec<u8>>; 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JacketCache {
|
impl JacketCache {
|
||||||
|
@ -96,7 +97,7 @@ impl JacketCache {
|
||||||
|
|
||||||
let mut jackets = Vec::new();
|
let mut jackets = Vec::new();
|
||||||
let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory");
|
||||||
for entry in entries {
|
for (i, entry) in entries.enumerate() {
|
||||||
let dir = entry?;
|
let dir = entry?;
|
||||||
let raw_dir_name = dir.file_name();
|
let raw_dir_name = dir.file_name();
|
||||||
let dir_name = raw_dir_name.to_str().unwrap();
|
let dir_name = raw_dir_name.to_str().unwrap();
|
||||||
|
@ -127,6 +128,11 @@ impl JacketCache {
|
||||||
jackets.push((file.path(), song.id));
|
jackets.push((file.path(), song.id));
|
||||||
|
|
||||||
let contents = fs::read(file.path())?.leak();
|
let contents = fs::read(file.path())?.leak();
|
||||||
|
let bitmap = Box::leak(Box::new(
|
||||||
|
image::load_from_memory(contents)?
|
||||||
|
.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest)
|
||||||
|
.into_rgb8(),
|
||||||
|
));
|
||||||
|
|
||||||
if name == "base" {
|
if name == "base" {
|
||||||
let item = song_cache.lookup_mut(song.id).unwrap();
|
let item = song_cache.lookup_mut(song.id).unwrap();
|
||||||
|
@ -156,14 +162,20 @@ impl JacketCache {
|
||||||
if !specialized_path.exists() && !dest.exists() {
|
if !specialized_path.exists() && !dest.exists() {
|
||||||
std::os::unix::fs::symlink(file.path(), dest)
|
std::os::unix::fs::symlink(file.path(), dest)
|
||||||
.expect("Could not symlink jacket");
|
.expect("Could not symlink jacket");
|
||||||
chart.cached_jacket = Some(contents);
|
chart.cached_jacket = Some(Jacket {
|
||||||
|
raw: contents,
|
||||||
|
bitmap,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if difficulty.is_some() {
|
} else if difficulty.is_some() {
|
||||||
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
||||||
.expect("Could not symlink jacket");
|
.expect("Could not symlink jacket");
|
||||||
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
||||||
chart.cached_jacket = Some(contents);
|
chart.cached_jacket = Some(Jacket {
|
||||||
|
raw: contents,
|
||||||
|
bitmap,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,8 +192,36 @@ impl JacketCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let assets_dir = data_dir.join("assets");
|
||||||
|
|
||||||
|
let lib = Library::init()?;
|
||||||
|
let saira_font = lib.new_face(assets_dir.join("saira-variable.ttf"), 0)?;
|
||||||
|
let mut canvas = BitmapCanvas::new(0, 0);
|
||||||
|
canvas.text(
|
||||||
|
(0, 0),
|
||||||
|
saira_font,
|
||||||
|
20,
|
||||||
|
"Yo, this is a test!",
|
||||||
|
(0, 0, 0, 0xff),
|
||||||
|
)?;
|
||||||
|
|
||||||
let result = Self {
|
let result = Self {
|
||||||
tree: KdMap::build_by_ordered_float(entries),
|
tree: KdMap::build_by_ordered_float(entries),
|
||||||
|
b30_background: image::open(assets_dir.join("b30_background.jpg"))?
|
||||||
|
.resize(2048 * 2, 1535 * 2, FilterType::Nearest)
|
||||||
|
.blur(20.0)
|
||||||
|
.into_rgb8(),
|
||||||
|
count_background: image::open(assets_dir.join("count_background.png"))?
|
||||||
|
.blur(1.0)
|
||||||
|
.into_rgba8(),
|
||||||
|
diff_backgrounds: Difficulty::DIFFICULTY_SHORTHANDS.try_map(
|
||||||
|
|shorthand| -> Result<_, Error> {
|
||||||
|
Ok(image::open(
|
||||||
|
assets_dir.join(format!("diff-{}.png", shorthand.to_lowercase())),
|
||||||
|
)?
|
||||||
|
.into_rgba8())
|
||||||
|
},
|
||||||
|
)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
#![warn(clippy::str_to_string)]
|
#![warn(clippy::str_to_string)]
|
||||||
#![feature(iter_map_windows)]
|
#![feature(iter_map_windows)]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
|
#![feature(array_try_map)]
|
||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
|
|
||||||
|
mod bitmap;
|
||||||
mod chart;
|
mod chart;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod context;
|
mod context;
|
||||||
|
|
30
src/score.rs
30
src/score.rs
|
@ -153,8 +153,16 @@ impl Score {
|
||||||
|
|
||||||
// Compute score from note breakdown subpairs
|
// Compute score from note breakdown subpairs
|
||||||
let pf_score = Score::compute_naive(note_count, pures, fars);
|
let pf_score = Score::compute_naive(note_count, pures, fars);
|
||||||
let fl_score = Score::compute_naive(note_count, note_count - losts - fars, fars);
|
let fl_score = Score::compute_naive(
|
||||||
let lp_score = Score::compute_naive(note_count, pures, note_count - losts - pures);
|
note_count,
|
||||||
|
note_count.checked_sub(losts + fars).unwrap_or(0),
|
||||||
|
fars,
|
||||||
|
);
|
||||||
|
let lp_score = Score::compute_naive(
|
||||||
|
note_count,
|
||||||
|
pures,
|
||||||
|
note_count.checked_sub(losts + pures).unwrap_or(0),
|
||||||
|
);
|
||||||
|
|
||||||
if no_shiny_scores.len() == 1 {
|
if no_shiny_scores.len() == 1 {
|
||||||
// {{{ Score is fixed, gotta figure out the exact distribution
|
// {{{ Score is fixed, gotta figure out the exact distribution
|
||||||
|
@ -450,14 +458,14 @@ impl Play {
|
||||||
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
|
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
|
||||||
if let Some(fars) = self.far_notes {
|
if let Some(fars) = self.far_notes {
|
||||||
let (_, shinies, units) = self.score.analyse(note_count);
|
let (_, shinies, units) = self.score.analyse(note_count);
|
||||||
let (pures, rem) = (units - fars).div_rem_euclid(&2);
|
let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2);
|
||||||
if rem == 1 {
|
if rem == 1 {
|
||||||
println!("The impossible happened: got an invalid amount of far notes!");
|
println!("The impossible happened: got an invalid amount of far notes!");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lost = note_count - fars - pures;
|
let lost = note_count.checked_sub(fars + pures)?;
|
||||||
let non_max_pures = pures - shinies;
|
let non_max_pures = pures.checked_sub(shinies)?;
|
||||||
Some((shinies, non_max_pures, fars, lost))
|
Some((shinies, non_max_pures, fars, lost))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -474,7 +482,7 @@ impl Play {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let non_max_pures = chart.note_count + 10_000_000 - score;
|
let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
|
||||||
if non_max_pures == 0 {
|
if non_max_pures == 0 {
|
||||||
Some("MPM".to_string())
|
Some("MPM".to_string())
|
||||||
} else {
|
} else {
|
||||||
|
@ -507,8 +515,8 @@ impl Play {
|
||||||
author: Option<&poise::serenity_prelude::User>,
|
author: Option<&poise::serenity_prelude::User>,
|
||||||
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
||||||
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
||||||
let icon_attachement = match chart.cached_jacket {
|
let icon_attachement = match chart.cached_jacket.as_ref() {
|
||||||
Some(bytes) => Some(CreateAttachment::bytes(bytes, &attachement_name)),
|
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -527,16 +535,16 @@ impl Play {
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field("Grade", self.score.grade(), true)
|
.field("Grade", self.score.grade(), true)
|
||||||
.field("ζ-Score", format!("{} (+?)", self.zeta_score), true)
|
.field("ξ-Score", format!("{} (+?)", self.zeta_score), true)
|
||||||
.field(
|
.field(
|
||||||
"ζ-Rating",
|
"ξ-Rating",
|
||||||
format!(
|
format!(
|
||||||
"{:.2} (+?)",
|
"{:.2} (+?)",
|
||||||
(self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100.
|
(self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100.
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field("ζ-Grade", self.zeta_score.grade(), true)
|
.field("ξ-Grade", self.zeta_score.grade(), true)
|
||||||
.field(
|
.field(
|
||||||
"Status",
|
"Status",
|
||||||
self.status(chart).unwrap_or("?".to_string()),
|
self.status(chart).unwrap_or("?".to_string()),
|
||||||
|
|
Loading…
Reference in a new issue