mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-03-04 17:23:33 +01:00
core: split TileMerger out of TileMipmapper
Reusable mipmap-style tile merging
This commit is contained in:
parent
7740ce0522
commit
0f308788ef
3 changed files with 220 additions and 106 deletions
|
@ -5,6 +5,7 @@ mod metadata_writer;
|
|||
mod region_group;
|
||||
mod region_processor;
|
||||
mod tile_collector;
|
||||
mod tile_merger;
|
||||
mod tile_mipmapper;
|
||||
mod tile_renderer;
|
||||
|
||||
|
|
97
src/core/tile_merger.rs
Normal file
97
src/core/tile_merger.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
//! Mipmap-style merging of tiles
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::BufWriter,
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing::warn;
|
||||
|
||||
use super::common::*;
|
||||
use crate::io::fs;
|
||||
|
||||
/// [TileMerger::merge_tiles] return
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Stat {
|
||||
/// None of the input files were found
|
||||
NotFound,
|
||||
/// The output file is up-to-date
|
||||
Skipped,
|
||||
/// The output file is regenerated
|
||||
Regenerate,
|
||||
}
|
||||
|
||||
/// A source file for the [TileMerger]
|
||||
///
|
||||
/// The tuple elements are X and Z coordinate offsets in the range [0, 1],
|
||||
/// the file path and the time of last change of the input.
|
||||
pub type Source = ((i32, i32), PathBuf, SystemTime);
|
||||
|
||||
/// Reusable trait for mipmap-style tile merging with change tracking
|
||||
pub trait TileMerger {
|
||||
/// [fs::FileMetaVersion] of input and output files
|
||||
///
|
||||
/// The version in the file metadata on disk must match the returned
|
||||
/// version for the a to be considered up-to-date.
|
||||
fn file_meta_version(&self) -> fs::FileMetaVersion;
|
||||
|
||||
/// Returns the paths of input and output files
|
||||
fn tile_path(&self, level: usize, coords: TileCoords) -> PathBuf;
|
||||
|
||||
/// Can be used to log the processing status
|
||||
fn log(&self, _output_path: &Path, _stat: Stat) {}
|
||||
|
||||
/// Handles the actual merging of source files
|
||||
fn write_tile(&self, file: &mut BufWriter<File>, sources: &[Source]) -> Result<()>;
|
||||
|
||||
/// Generates a tile at given coordinates and mipmap level
|
||||
fn merge_tiles(&self, level: usize, coords: TileCoords, prev: &TileCoordMap) -> Result<Stat> {
|
||||
let version = self.file_meta_version();
|
||||
let output_path = self.tile_path(level, coords);
|
||||
let output_timestamp = fs::read_timestamp(&output_path, version);
|
||||
|
||||
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
.into_iter()
|
||||
.filter_map(|(dx, dz)| {
|
||||
let source_coords = TileCoords {
|
||||
x: 2 * coords.x + dx,
|
||||
z: 2 * coords.z + dz,
|
||||
};
|
||||
if !prev.contains(source_coords) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source_path = self.tile_path(level - 1, source_coords);
|
||||
let timestamp = match fs::modified_timestamp(&source_path) {
|
||||
Ok(timestamp) => timestamp,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(((dx, dz), source_path, timestamp))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
|
||||
self.log(&output_path, Stat::NotFound);
|
||||
return Ok(Stat::NotFound);
|
||||
};
|
||||
|
||||
if Some(input_timestamp) <= output_timestamp {
|
||||
self.log(&output_path, Stat::Skipped);
|
||||
return Ok(Stat::Skipped);
|
||||
}
|
||||
|
||||
self.log(&output_path, Stat::Regenerate);
|
||||
|
||||
fs::create_with_timestamp(&output_path, version, input_timestamp, |file| {
|
||||
self.write_tile(file, &sources)
|
||||
})?;
|
||||
|
||||
Ok(Stat::Regenerate)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
//! The [TileMipmapper]
|
||||
|
||||
use std::ops::Add;
|
||||
use std::{marker::PhantomData, ops::Add};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::{common::*, tile_collector::TileCollector};
|
||||
use super::{
|
||||
common::*,
|
||||
tile_collector::TileCollector,
|
||||
tile_merger::{self, TileMerger},
|
||||
};
|
||||
use crate::{io::fs, types::*};
|
||||
|
||||
/// Counters for the number of processed and total tiles
|
||||
|
@ -19,22 +23,23 @@ pub struct MipmapStat {
|
|||
processed: usize,
|
||||
}
|
||||
|
||||
impl MipmapStat {
|
||||
/// Mipmap step return when none of the input files exist
|
||||
const NOT_FOUND: MipmapStat = MipmapStat {
|
||||
impl From<tile_merger::Stat> for MipmapStat {
|
||||
fn from(value: tile_merger::Stat) -> Self {
|
||||
match value {
|
||||
tile_merger::Stat::NotFound => MipmapStat {
|
||||
total: 0,
|
||||
processed: 0,
|
||||
};
|
||||
/// Mipmap step return when output file is up-to-date
|
||||
const SKIPPED: MipmapStat = MipmapStat {
|
||||
},
|
||||
tile_merger::Stat::Skipped => MipmapStat {
|
||||
total: 1,
|
||||
processed: 0,
|
||||
};
|
||||
/// Mipmap step return when a new output file has been generated
|
||||
const PROCESSED: MipmapStat = MipmapStat {
|
||||
},
|
||||
tile_merger::Stat::Regenerate => MipmapStat {
|
||||
total: 1,
|
||||
processed: 1,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for MipmapStat {
|
||||
|
@ -48,6 +53,102 @@ impl Add for MipmapStat {
|
|||
}
|
||||
}
|
||||
|
||||
/// [TileMerger] for map tile images
|
||||
struct MapMerger<'a, P> {
|
||||
/// Common MinedMap configuration from command line
|
||||
config: &'a Config,
|
||||
/// Tile kind (map or lightmap)
|
||||
kind: TileKind,
|
||||
/// Pixel format type
|
||||
_pixel: PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<'a, P> MapMerger<'a, P> {
|
||||
/// Creates a new [MapMerger]
|
||||
fn new(config: &'a Config, kind: TileKind) -> Self {
|
||||
MapMerger {
|
||||
config,
|
||||
kind,
|
||||
_pixel: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: image::PixelWithColorType> TileMerger for MapMerger<'a, P>
|
||||
where
|
||||
[P::Subpixel]: image::EncodableLayout,
|
||||
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
|
||||
{
|
||||
fn file_meta_version(&self) -> fs::FileMetaVersion {
|
||||
MIPMAP_FILE_META_VERSION
|
||||
}
|
||||
|
||||
fn tile_path(&self, level: usize, coords: TileCoords) -> std::path::PathBuf {
|
||||
self.config.tile_path(self.kind, level, coords)
|
||||
}
|
||||
|
||||
fn log(&self, output_path: &std::path::Path, stat: super::tile_merger::Stat) {
|
||||
match stat {
|
||||
super::tile_merger::Stat::NotFound => {}
|
||||
super::tile_merger::Stat::Skipped => {
|
||||
debug!(
|
||||
"Skipping unchanged mipmap tile {}",
|
||||
output_path
|
||||
.strip_prefix(&self.config.output_dir)
|
||||
.expect("tile path must be in output directory")
|
||||
.display(),
|
||||
);
|
||||
}
|
||||
super::tile_merger::Stat::Regenerate => {
|
||||
debug!(
|
||||
"Rendering mipmap tile {}",
|
||||
output_path
|
||||
.strip_prefix(&self.config.output_dir)
|
||||
.expect("tile path must be in output directory")
|
||||
.display(),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn write_tile(
|
||||
&self,
|
||||
file: &mut std::io::BufWriter<std::fs::File>,
|
||||
sources: &[super::tile_merger::Source],
|
||||
) -> Result<()> {
|
||||
/// Tile width/height
|
||||
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
||||
|
||||
let mut image: image::DynamicImage =
|
||||
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
|
||||
|
||||
for ((dx, dz), source_path, _) in sources {
|
||||
let source = match image::open(source_path) {
|
||||
Ok(source) => source,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to read source image {}: {}",
|
||||
source_path.display(),
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
|
||||
image::imageops::overlay(
|
||||
&mut image,
|
||||
&resized,
|
||||
*dx as i64 * (N / 2) as i64,
|
||||
*dz as i64 * (N / 2) as i64,
|
||||
);
|
||||
}
|
||||
|
||||
image
|
||||
.write_to(file, image::ImageFormat::Png)
|
||||
.context("Failed to save image")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates mipmap tiles from full-resolution tile images
|
||||
pub struct TileMipmapper<'a> {
|
||||
/// Common MinedMap configuration from command line
|
||||
|
@ -128,94 +229,9 @@ impl<'a> TileMipmapper<'a> {
|
|||
[P::Subpixel]: image::EncodableLayout,
|
||||
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
|
||||
{
|
||||
/// Tile width/height
|
||||
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
|
||||
|
||||
let output_path = self.config.tile_path(kind, level, coords);
|
||||
let output_timestamp = fs::read_timestamp(&output_path, MIPMAP_FILE_META_VERSION);
|
||||
|
||||
let sources: Vec<_> = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
.into_iter()
|
||||
.filter_map(|(dx, dz)| {
|
||||
let source_coords = TileCoords {
|
||||
x: 2 * coords.x + dx,
|
||||
z: 2 * coords.z + dz,
|
||||
};
|
||||
if !prev.contains(source_coords) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source_path = self.config.tile_path(kind, level - 1, source_coords);
|
||||
let timestamp = match fs::modified_timestamp(&source_path) {
|
||||
Ok(timestamp) => timestamp,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(((dx, dz), source_path, timestamp))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let Some(input_timestamp) = sources.iter().map(|(_, _, ts)| *ts).max() else {
|
||||
return Ok(MipmapStat::NOT_FOUND);
|
||||
};
|
||||
|
||||
if Some(input_timestamp) <= output_timestamp {
|
||||
debug!(
|
||||
"Skipping unchanged mipmap tile {}",
|
||||
output_path
|
||||
.strip_prefix(&self.config.output_dir)
|
||||
.expect("tile path must be in output directory")
|
||||
.display(),
|
||||
);
|
||||
return Ok(MipmapStat::SKIPPED);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Rendering mipmap tile {}",
|
||||
output_path
|
||||
.strip_prefix(&self.config.output_dir)
|
||||
.expect("tile path must be in output directory")
|
||||
.display(),
|
||||
);
|
||||
|
||||
let mut image: image::DynamicImage =
|
||||
image::ImageBuffer::<P, Vec<P::Subpixel>>::new(N, N).into();
|
||||
|
||||
for ((dx, dz), source_path, _) in sources {
|
||||
let source = match image::open(&source_path) {
|
||||
Ok(source) => source,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to read source image {}: {}",
|
||||
source_path.display(),
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let resized = source.resize(N / 2, N / 2, image::imageops::FilterType::Triangle);
|
||||
image::imageops::overlay(
|
||||
&mut image,
|
||||
&resized,
|
||||
dx as i64 * (N / 2) as i64,
|
||||
dz as i64 * (N / 2) as i64,
|
||||
);
|
||||
}
|
||||
|
||||
fs::create_with_timestamp(
|
||||
&output_path,
|
||||
MIPMAP_FILE_META_VERSION,
|
||||
input_timestamp,
|
||||
|file| {
|
||||
image
|
||||
.write_to(file, image::ImageFormat::Png)
|
||||
.context("Failed to save image")
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(MipmapStat::PROCESSED)
|
||||
let merger = MapMerger::<P>::new(self.config, kind);
|
||||
let ret = merger.merge_tiles(level, coords, prev)?;
|
||||
Ok(ret.into())
|
||||
}
|
||||
|
||||
/// Runs the mipmap generation
|
||||
|
|
Loading…
Add table
Reference in a new issue