use std::{
collections::{HashMap, VecDeque},
path::PathBuf,
};
use bevy_app::{Last, Plugin, PreStartup};
use bevy_asset::{AssetServer, Assets, Handle, RenderAssetUsages};
use bevy_ecs::{
resource::Resource,
system::{Commands, Res, ResMut},
};
use bevy_image::Image;
use bevy_tasks::AsyncComputeTaskPool;
use serde::Serialize;
use serde_json::{Map, value::Serializer};
use typst::{diag::Severity, foundations::Dict, layout::PagedDocument};
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
use crate::asset_loading::{AssetPluginForTypstTextures, TypstTemplate};
pub mod asset_loading;
pub mod file_resolver;
#[derive(Debug, Clone, Resource, Default)]
pub struct TypstTexturesPlugin {
pub jobs_per_frame: Option<u32>,
}
impl Plugin for TypstTexturesPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_plugins(AssetPluginForTypstTextures);
app.insert_resource(self.clone());
app.add_systems(PreStartup, TypstTextureServer::system_insert_to_world)
.add_systems(Last, TypstTextureServer::system_do_jobs);
}
}
#[derive(Debug)]
pub struct TypstJob {
pub use_template: Handle<TypstTemplate>,
pub input: Dict,
pub send_target: async_channel::Sender<bevy_image::Image>,
pub job_options: TypstJobOptions,
_handle: Handle<Image>,
}
#[derive(Debug, Clone)]
pub struct TypstJobOptions {
pub pixels_per_pt: f32,
pub specific_page: Option<usize>,
pub asset_usage: RenderAssetUsages,
}
impl Default for TypstJobOptions {
fn default() -> Self {
Self {
pixels_per_pt: 1.0,
specific_page: None,
asset_usage: RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
}
}
}
#[derive(Debug, Resource)]
pub struct TypstTextureServer {
asset_server: AssetServer,
pub fallback: Image,
pub templates: HashMap<PathBuf, Handle<TypstTemplate>>,
pub jobs: VecDeque<TypstJob>,
pub jobs_per_frame: Option<u32>,
}
impl TypstTextureServer {
pub(crate) fn system_insert_to_world(
mut commands: Commands,
asset_server: Res<AssetServer>,
plugin_settings: Res<TypstTexturesPlugin>,
) {
let mut typst_template_server = Self::new(asset_server.clone());
typst_template_server.jobs_per_frame = plugin_settings.jobs_per_frame;
commands.remove_resource::<TypstTexturesPlugin>();
commands.insert_resource(typst_template_server);
}
pub fn system_do_jobs(
mut template_server: ResMut<TypstTextureServer>,
templates: Res<Assets<TypstTemplate>>,
) {
let max_jobs = template_server
.jobs_per_frame
.unwrap_or(template_server.jobs.len() as u32);
let mut jobs_done = 0;
let mut compiled_map = HashMap::new();
while jobs_done < max_jobs
&& let Some(job) = template_server.jobs.pop_front()
{
if template_server.asset_server.is_loaded(&job.use_template)
&& let Some(template) = templates.get(&job.use_template)
{
let (engine, _) = compiled_map
.entry(job.use_template.clone())
.or_insert_with(|| template.0.clone().to_engine());
let compiled = engine.compile_with_input::<_, PagedDocument>(job.input);
let path = job.use_template.path();
let Ok(page) = compiled.output else {
bevy_log::error!(
"[TYPST FATAL ERROR for {:?}] {}",
path,
compiled.output.unwrap_err()
);
continue;
};
for warning in compiled.warnings {
if warning.severity == Severity::Error {
bevy_log::error!("[TYPST ERROR for {:?}] {}", path, warning.message);
} else {
bevy_log::warn!("[TYPST WARNING for {:?}] {}", path, warning.message);
}
}
let rendered = typst_render::render(
&page.pages[job
.job_options
.specific_page
.map(|page_num| (page.pages.len().saturating_sub(1)).min(page_num))
.unwrap_or(0)],
job.job_options.pixels_per_pt,
);
let asset_usage = job.job_options.asset_usage;
let sender = job.send_target.clone();
AsyncComputeTaskPool::get()
.spawn(async move {
sender
.send(bevy_image::Image::new(
Extent3d {
width: rendered.width(),
height: rendered.height(),
depth_or_array_layers: 1,
},
TextureDimension::D2,
rendered.data().to_vec(),
TextureFormat::Rgba8UnormSrgb,
asset_usage,
))
.await
})
.detach();
} else {
template_server.jobs.push_back(job);
}
jobs_done += 1;
}
}
pub fn new(asset_server: AssetServer) -> Self {
Self::new_with_fallback(
asset_server,
bevy_image::Image::new(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
vec![255, 0, 255, 255],
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD,
),
)
}
pub fn new_with_fallback(asset_server: AssetServer, fallback: Image) -> Self {
Self {
asset_server,
fallback,
templates: HashMap::new(),
jobs: VecDeque::new(),
jobs_per_frame: None,
}
}
pub fn add_job(&mut self, path: impl Into<PathBuf>, options: TypstJobOptions) -> Handle<Image> {
self.add_job_with_input(path, serde_json::Value::Object(Map::new()), options)
}
pub fn add_job_with_input(
&mut self,
path: impl Into<PathBuf>,
input: impl Serialize,
options: TypstJobOptions,
) -> Handle<Image> {
let asset_server = self.asset_server.clone();
let path_buf = path.into();
let template = self
.templates
.entry(path_buf.clone())
.or_insert_with(|| asset_server.load(path_buf));
let (sender, receiver) = async_channel::unbounded::<bevy_image::Image>();
let handle: Handle<Image> = self.asset_server.add_async(async move {
let res = receiver.recv().await;
if let Err(res) = &res {
bevy_log::error!("[TYPST ASYNC JOB ERROR] {res}")
}
res
});
let Ok(input): Result<serde_json::Value, _> = input.serialize(Serializer) else {
bevy_log::error!(
"[TYPST INPUT ERROR] Could not transform value into a serde json as interim for Dict."
);
return handle;
};
let Ok(input) = serde_json::from_value(input) else {
bevy_log::error!("[TYPST INPUT ERROR] Could not get Dict from interim serde json.");
return handle;
};
self.jobs.push_back(TypstJob {
use_template: template.clone(),
input,
send_target: sender,
job_options: options,
_handle: handle.clone(),
});
handle
}
pub fn limit_jobs(mut self, limit: u32) -> Self {
self.jobs_per_frame = Some(limit);
self
}
pub fn unlimited_jobs(mut self) -> Self {
self.jobs_per_frame = None;
self
}
}