use std::collections::HashMap;
use std::fmt;
use bevy_ecs::prelude::*;
use tracing::debug;
use windows::core::Interface;
use super::metrics::Size;
use crate::com::wic::{
WICBitmapDecoderExt, WICBitmapSourceExt, WICFormatConverterExt, WICImagingFactoryExt,
wic_factory,
};
#[derive(Debug)]
pub enum HitRegionError {
InsufficientVertices { vertices: usize },
InvalidRectSize { width: f32, height: f32 },
ImageLoadFailed(windows::core::Error),
}
impl fmt::Display for HitRegionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HitRegionError::InsufficientVertices { vertices } => {
write!(f, "多角形の頂点数が不足しています: {} < 3", vertices)
}
HitRegionError::InvalidRectSize { width, height } => {
write!(
f,
"矩形のサイズが不正です: width={}, height={}",
width, height
)
}
HitRegionError::ImageLoadFailed(e) => {
write!(f, "カラーマップ画像の読込に失敗しました: {}", e)
}
}
}
}
impl std::error::Error for HitRegionError {}
impl From<windows::core::Error> for HitRegionError {
fn from(e: windows::core::Error) -> Self {
HitRegionError::ImageLoadFailed(e)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Shape {
Rect {
x: f32,
y: f32,
width: f32,
height: f32,
},
Polygon { vertices: Vec<(f32, f32)> },
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShapeRegion {
pub name: String,
pub shape: Shape,
}
#[derive(Debug, Clone)]
pub struct ColorMapData {
index_map: Vec<u8>,
region_names: Vec<String>,
width: u32,
height: u32,
}
impl ColorMapData {
pub fn from_image(
image_path: &std::path::Path,
mapping: &HashMap<(u8, u8, u8), String>,
) -> windows::core::Result<Self> {
use windows::Win32::Foundation::GENERIC_READ;
use windows::Win32::Graphics::Imaging::*;
use windows::core::HSTRING;
let mut region_names = Vec::new();
let mut color_to_id: HashMap<(u8, u8, u8), u8> = HashMap::new();
for (color, name) in mapping {
let id = (region_names.len() + 1) as u8; region_names.push(name.clone());
color_to_id.insert(*color, id);
}
let factory = wic_factory()?;
let path_str = image_path.to_string_lossy();
let path_hstring = HSTRING::from(path_str.as_ref());
let decoder = factory.create_decoder_from_filename(
&path_hstring,
None,
GENERIC_READ,
WICDecodeMetadataCacheOnDemand,
)?;
let frame = decoder.frame(0)?;
let converter = factory.create_format_converter()?;
converter.init(
&frame.cast::<IWICBitmapSource>()?,
&GUID_WICPixelFormat32bppPBGRA,
WICBitmapDitherTypeNone,
None,
0.0,
WICBitmapPaletteTypeCustom,
)?;
let converted: IWICBitmapSource = converter.cast()?;
let (width, height) = converted.get_size()?;
let stride = width * 4; let buffer_size = (stride * height) as usize;
let mut buffer = vec![0u8; buffer_size];
converted.copy_pixels(None, stride, &mut buffer)?;
let pixel_count = (width * height) as usize;
let mut index_map = vec![0u8; pixel_count];
for i in 0..pixel_count {
let offset = i * 4;
let b = buffer[offset];
let g = buffer[offset + 1];
let r = buffer[offset + 2];
if let Some(&id) = color_to_id.get(&(r, g, b)) {
index_map[i] = id;
}
}
debug!(
width = width,
height = height,
regions = region_names.len(),
"カラーマップ画像読込完了"
);
Ok(Self {
index_map,
region_names,
width,
height,
})
}
pub fn hit_test(&self, pixel_x: u32, pixel_y: u32) -> Option<&str> {
if pixel_x >= self.width || pixel_y >= self.height {
return None;
}
let index = (pixel_y * self.width + pixel_x) as usize;
debug_assert!(
index < self.index_map.len(),
"ColorMapData invariant violated: index_map.len() must equal width*height"
);
let region_id = self.index_map[index];
if region_id == 0 {
None
} else {
self.region_names
.get((region_id - 1) as usize)
.map(|s| s.as_str())
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColorMapDef {
pub image_path: String,
pub mapping: Vec<ColorMapping>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColorMapping {
pub color: [u8; 3],
pub name: String,
}
#[derive(Debug, Clone)]
enum RegionKind {
Shapes(Vec<ShapeRegion>),
ColorMap(ColorMapData),
}
#[derive(Component, Debug, Clone)]
pub struct HitRegionMap {
kind: RegionKind,
}
impl HitRegionMap {
pub fn builder() -> HitRegionMapBuilder {
HitRegionMapBuilder::new()
}
pub fn from_color_map(
image_path: &std::path::Path,
mapping: &HashMap<(u8, u8, u8), String>,
) -> Result<Self, HitRegionError> {
let data = ColorMapData::from_image(image_path, mapping)?;
Ok(Self {
kind: RegionKind::ColorMap(data),
})
}
pub fn hit_test_region(&self, rel_x: f32, rel_y: f32, entity_size: &Size) -> Option<&str> {
match &self.kind {
RegionKind::Shapes(regions) => {
let local_x = rel_x * entity_size.width;
let local_y = rel_y * entity_size.height;
for region in regions {
let hit = match ®ion.shape {
Shape::Rect {
x,
y,
width,
height,
} => {
local_x >= *x
&& local_x <= *x + *width
&& local_y >= *y
&& local_y <= *y + *height
}
Shape::Polygon { vertices } => point_in_polygon(local_x, local_y, vertices),
};
if hit {
return Some(®ion.name);
}
}
None
}
RegionKind::ColorMap(data) => {
let pixel_x = (rel_x * data.width() as f32) as u32;
let pixel_y = (rel_y * data.height() as f32) as u32;
data.hit_test(pixel_x, pixel_y)
}
}
}
}
pub struct HitRegionMapBuilder {
regions: Vec<ShapeRegion>,
}
impl HitRegionMapBuilder {
pub fn new() -> Self {
Self {
regions: Vec::new(),
}
}
pub fn rect(mut self, name: &str, x: f32, y: f32, width: f32, height: f32) -> Self {
self.regions.push(ShapeRegion {
name: name.to_string(),
shape: Shape::Rect {
x,
y,
width,
height,
},
});
self
}
pub fn polygon(mut self, name: &str, vertices: &[(f32, f32)]) -> Self {
self.regions.push(ShapeRegion {
name: name.to_string(),
shape: Shape::Polygon {
vertices: vertices.to_vec(),
},
});
self
}
pub fn build(self) -> Result<HitRegionMap, HitRegionError> {
for region in &self.regions {
match ®ion.shape {
Shape::Rect { width, height, .. } => {
if *width <= 0.0 || *height <= 0.0 {
return Err(HitRegionError::InvalidRectSize {
width: *width,
height: *height,
});
}
}
Shape::Polygon { vertices } => {
if vertices.len() < 3 {
return Err(HitRegionError::InsufficientVertices {
vertices: vertices.len(),
});
}
}
}
}
Ok(HitRegionMap {
kind: RegionKind::Shapes(self.regions),
})
}
}
impl Default for HitRegionMapBuilder {
fn default() -> Self {
Self::new()
}
}
fn point_in_polygon(x: f32, y: f32, vertices: &[(f32, f32)]) -> bool {
let n = vertices.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = vertices[i];
let (xj, yj) = vertices[j];
if (yi > y) != (yj > y) {
let intersect_x = xj + (y - yj) / (yi - yj) * (xi - xj);
if x < intersect_x {
inside = !inside;
}
}
j = i;
}
inside
}
#[cfg(test)]
mod tests;