Unlucky Launcher v0.2.1
Single-commit source snapshot of the v0.2.1 release.
This commit is contained in:
commit
d517f28140
1092 files changed
+211068
No files matched your search
@@ -0,0 +1,183 @@
|
||||
//! 截图像素扫描工具(`gallery::probe` 的底层,四个后续家族的 probe 也能复用)。
|
||||
//!
|
||||
//! 只做「按颜色找连通块 / 量外接框 / 量某行的连续长度」三件事,不含任何判据——
|
||||
//! 判据在同级 `probe.rs` 里,这样每条家族线可以自带 probe 而不用重写扫描逻辑。
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use iced::Color;
|
||||
|
||||
/// 颜色比对的通道容差(抗锯齿/取整)。
|
||||
const TOLERANCE: u8 = 10;
|
||||
|
||||
/// iced 颜色 → 8bit RGB(截图里的原值)。
|
||||
pub(super) fn rgb(color: Color) -> [u8; 3] {
|
||||
[
|
||||
(color.r * 255.0).round() as u8,
|
||||
(color.g * 255.0).round() as u8,
|
||||
(color.b * 255.0).round() as u8,
|
||||
]
|
||||
}
|
||||
|
||||
/// 颜色是否落在容差内。
|
||||
pub(super) fn matches(pixel: [u8; 3], expected: [u8; 3]) -> bool {
|
||||
pixel
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.all(|(a, b)| a.abs_diff(b) <= TOLERANCE)
|
||||
}
|
||||
|
||||
/// 一张图的像素访问器。
|
||||
pub(super) struct Shot {
|
||||
pub(super) width: usize,
|
||||
pub(super) height: usize,
|
||||
pixels: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
impl Shot {
|
||||
pub(super) fn load(path: &Path) -> Result<Self, String> {
|
||||
let bytes = std::fs::read(path).map_err(|error| format!("读不动 {path:?}:{error}"))?;
|
||||
let image = image::load_from_memory(&bytes)
|
||||
.map_err(|error| format!("解 PNG 失败:{error}"))?
|
||||
.to_rgb8();
|
||||
let (width, height) = (image.width() as usize, image.height() as usize);
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
pixels: image.pixels().map(|p| p.0).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn at(&self, x: usize, y: usize) -> [u8; 3] {
|
||||
self.pixels[y * self.width + x]
|
||||
}
|
||||
|
||||
/// 某种颜色的全部像素的外接框(连通块)。
|
||||
pub(super) fn bbox_of(&self, color: [u8; 3]) -> Vec<BBox> {
|
||||
let mut seen = vec![false; self.width * self.height];
|
||||
let mut boxes = Vec::new();
|
||||
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
if seen[y * self.width + x] || !matches(self.at(x, y), color) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut stack = vec![(x, y)];
|
||||
seen[y * self.width + x] = true;
|
||||
let mut bbox = BBox::new(x, y);
|
||||
|
||||
while let Some((cx, cy)) = stack.pop() {
|
||||
bbox.include(cx, cy);
|
||||
for (nx, ny) in [
|
||||
(cx + 1, cy),
|
||||
(cx.saturating_sub(1), cy),
|
||||
(cx, cy + 1),
|
||||
(cx, cy.saturating_sub(1)),
|
||||
] {
|
||||
let inside = nx < self.width && ny < self.height;
|
||||
if inside && !seen[ny * self.width + nx] && matches(self.at(nx, ny), color)
|
||||
{
|
||||
seen[ny * self.width + nx] = true;
|
||||
stack.push((nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bbox.area() > 200 {
|
||||
boxes.push(bbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boxes
|
||||
}
|
||||
|
||||
/// 在矩形 `[x0,x1) × [y0,y1)` 内数某颜色的像素(按行/列取证用)。
|
||||
pub(super) fn count_in(
|
||||
&self,
|
||||
color: [u8; 3],
|
||||
x0: usize,
|
||||
x1: usize,
|
||||
y0: usize,
|
||||
y1: usize,
|
||||
) -> usize {
|
||||
(y0..y1.min(self.height))
|
||||
.map(|y| {
|
||||
(x0..x1.min(self.width))
|
||||
.filter(|x| matches(self.at(*x, y), color))
|
||||
.count()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// `bbox` 内某颜色的墨迹外接框(量字形用)。
|
||||
///
|
||||
/// 内缩 3px:不内缩会把按钮自身的抗锯齿边算成墨迹(实测把 24 盒字形量成 40)。
|
||||
pub(super) fn ink_inside(&self, bbox: &BBox, color: [u8; 3]) -> Option<BBox> {
|
||||
const MARGIN: usize = 3;
|
||||
let mut ink: Option<BBox> = None;
|
||||
for y in (bbox.top + MARGIN)..=(bbox.bottom - MARGIN) {
|
||||
for x in (bbox.left + MARGIN)..=(bbox.right - MARGIN) {
|
||||
if matches(self.at(x, y), color) {
|
||||
match &mut ink {
|
||||
Some(found) => found.include(x, y),
|
||||
None => ink = Some(BBox::new(x, y)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ink
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个外接框(闭区间)。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub(super) struct BBox {
|
||||
pub(super) left: usize,
|
||||
pub(super) right: usize,
|
||||
pub(super) top: usize,
|
||||
pub(super) bottom: usize,
|
||||
}
|
||||
|
||||
impl BBox {
|
||||
pub(super) fn new(x: usize, y: usize) -> Self {
|
||||
Self {
|
||||
left: x,
|
||||
right: x,
|
||||
top: y,
|
||||
bottom: y,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn include(&mut self, x: usize, y: usize) {
|
||||
self.left = self.left.min(x);
|
||||
self.right = self.right.max(x);
|
||||
self.top = self.top.min(y);
|
||||
self.bottom = self.bottom.max(y);
|
||||
}
|
||||
|
||||
pub(super) fn width(&self) -> usize {
|
||||
self.right - self.left + 1
|
||||
}
|
||||
|
||||
pub(super) fn height(&self) -> usize {
|
||||
self.bottom - self.top + 1
|
||||
}
|
||||
|
||||
pub(super) fn area(&self) -> usize {
|
||||
self.width() * self.height()
|
||||
}
|
||||
}
|
||||
|
||||
/// `pixel` 更接近 `fill` 还是 `surface`。
|
||||
pub(super) fn nearer(pixel: [u8; 3], fill: [u8; 3], surface: [u8; 3]) -> bool {
|
||||
let dist = |a: [u8; 3], b: [u8; 3]| -> u32 {
|
||||
a.iter()
|
||||
.zip(b)
|
||||
.map(|(x, y)| (i32::from(*x) - i32::from(y)).unsigned_abs())
|
||||
.sum()
|
||||
};
|
||||
dist(pixel, fill) < dist(pixel, surface)
|
||||
}
|
||||
Reference in new issue
Block a user