use std::any::TypeId;
use std::collections::{HashMap, HashSet};
use std::fmt::{self, Formatter};
use std::path::{Path, PathBuf};
use bevy::asset::{
AssetLoader, LoadContext, ReflectAsset, ReflectHandle, UntypedHandle, io::Reader,
};
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::image::ImageLoaderSettings;
use bevy::prelude::*;
use bevy::reflect::serde::{ReflectDeserializerProcessor, TypedReflectDeserializer};
use bevy::reflect::{TypeRegistration, TypeRegistry};
use jackdaw_jsn::JsnPlugin;
use jackdaw_jsn::format::{JsnAssets, JsnScene, JsnSceneV2};
use serde::Deserializer;
use serde::de::{DeserializeSeed, Visitor};
pub use jackdaw_jsn::{
Brush, BrushFaceData, CustomProperties, EditorCategory, EditorDescription, EditorHidden,
GltfSource, PropertyValue, SkipSerialization,
};
pub mod prelude {
pub use crate::{
EditorCategory, EditorDescription, EditorHidden, JackdawPlugin, JackdawSceneRoot,
SkipSerialization,
};
}
pub struct JackdawPlugin;
impl Plugin for JackdawPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(JsnPlugin::default());
app.init_asset::<JackdawScene>()
.init_asset_loader::<JackdawSceneLoader>();
app.add_systems(
Update,
(clear_modified_scene_roots, spawn_loaded_scenes).chain(),
);
}
}
#[derive(Asset, TypePath)]
pub struct JackdawScene {
jsn: JsnScene,
parent_path: PathBuf,
}
impl JackdawScene {
pub fn new(jsn: JsnScene, parent_path: PathBuf) -> Self {
Self { jsn, parent_path }
}
}
#[derive(Component, Deref)]
#[require(Transform, Visibility)]
pub struct JackdawSceneRoot(pub Handle<JackdawScene>);
#[derive(Component)]
struct SceneSpawned;
#[derive(Debug, TypePath)]
struct JackdawSceneLoader;
impl FromWorld for JackdawSceneLoader {
fn from_world(_world: &mut World) -> Self {
Self
}
}
impl AssetLoader for JackdawSceneLoader {
type Asset = JackdawScene;
type Settings = ();
type Error = JackdawLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.await
.map_err(|e| JackdawLoadError::Io(e.to_string()))?;
let text =
std::str::from_utf8(&bytes).map_err(|e| JackdawLoadError::Parse(e.to_string()))?;
let jsn: JsnScene = match serde_json::from_str(text) {
Ok(jsn) => jsn,
Err(v3_err) => match serde_json::from_str::<JsnSceneV2>(text) {
Ok(v2) => v2.migrate_to_v3(),
Err(_) => return Err(JackdawLoadError::Parse(v3_err.to_string())),
},
};
let parent_path = load_context
.path()
.path()
.parent()
.unwrap_or(Path::new(""))
.to_owned();
Ok(JackdawScene { jsn, parent_path })
}
fn extensions(&self) -> &[&str] {
&["jsn"]
}
}
#[derive(Debug, thiserror::Error)]
pub enum JackdawLoadError {
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
}
fn clear_modified_scene_roots(
mut events: bevy::ecs::message::MessageReader<bevy::asset::AssetEvent<JackdawScene>>,
roots: Query<(Entity, &JackdawSceneRoot, Option<&Children>), With<SceneSpawned>>,
mut commands: Commands,
) {
use bevy::asset::AssetEvent;
let modified: Vec<bevy::asset::AssetId<JackdawScene>> = events
.read()
.filter_map(|event| match event {
AssetEvent::Modified { id } | AssetEvent::LoadedWithDependencies { id } => Some(*id),
_ => None,
})
.collect();
if modified.is_empty() {
return;
}
for (root_entity, root, kids) in &roots {
if !modified.contains(&root.0.id()) {
continue;
}
if let Some(kids) = kids {
for child in kids.iter() {
commands.entity(child).despawn();
}
}
commands.entity(root_entity).remove::<SceneSpawned>();
}
}
fn spawn_loaded_scenes(
world: &mut World,
scene_roots: &mut QueryState<(Entity, &JackdawSceneRoot), Without<SceneSpawned>>,
) {
let to_spawn: Vec<(Entity, Handle<JackdawScene>)> = scene_roots
.iter(world)
.map(|(e, root)| (e, root.0.clone()))
.collect();
for (root_entity, handle) in to_spawn {
let scenes = world.resource::<Assets<JackdawScene>>();
let Some(scene) = scenes.get(&handle) else {
continue;
};
let jsn = scene.jsn.clone();
let parent_path = scene.parent_path.clone();
let local_assets = load_inline_assets(world, &jsn.assets, &parent_path);
spawn_scene_entities(world, root_entity, &jsn.scene, &parent_path, &local_assets);
world.entity_mut(root_entity).insert(SceneSpawned);
}
}
const TRANSFORM_TYPE_PATH: &str = "bevy_transform::components::transform::Transform";
const VISIBILITY_TYPE_PATH: &str = "bevy_render::view::visibility::Visibility";
fn spawn_scene_entities(
world: &mut World,
root_entity: Entity,
entities: &[jackdaw_jsn::format::JsnEntity],
parent_path: &Path,
local_assets: &HashMap<String, UntypedHandle>,
) {
let registry = world.resource::<AppTypeRegistry>().clone();
let asset_server = world.resource::<AssetServer>().clone();
let topo: Vec<usize> = topological_index_order(entities);
let mut spawned: Vec<Entity> = vec![root_entity; entities.len()];
let registry_guard = registry.read();
for &i in &topo {
let jsn = &entities[i];
let parent_entity = match jsn.parent {
Some(p_idx) if p_idx < entities.len() => spawned[p_idx],
_ => root_entity,
};
let mut local_transform = Transform::default();
let mut local_visibility = Visibility::default();
let mut deferred: Vec<Box<dyn PartialReflect>> = Vec::new();
for (type_path, value) in &jsn.components {
let Some(registration) = registry_guard.get_with_type_path(type_path) else {
if !type_path.starts_with("jackdaw::") {
info!("Skipping unknown type '{type_path}' (not registered in runtime)");
}
continue;
};
if registration.data::<ReflectComponent>().is_none() {
continue;
}
let mut processor = RuntimeDeserializerProcessor {
asset_server: &asset_server,
parent_path,
local_assets,
entity_map: &spawned,
};
let deserializer = TypedReflectDeserializer::with_processor(
registration,
®istry_guard,
&mut processor,
);
let Ok(reflected) = deserializer.deserialize(value) else {
warn!("Failed to deserialize '{type_path}'; skipping");
continue;
};
if type_path == TRANSFORM_TYPE_PATH {
if let Some(t) =
<Transform as bevy::reflect::FromReflect>::from_reflect(reflected.as_ref())
{
local_transform = t;
}
} else if type_path == VISIBILITY_TYPE_PATH {
if let Some(v) =
<Visibility as bevy::reflect::FromReflect>::from_reflect(reflected.as_ref())
{
local_visibility = v;
}
} else {
deferred.push(reflected);
}
}
let parent_gt = world
.get::<GlobalTransform>(parent_entity)
.copied()
.unwrap_or(GlobalTransform::IDENTITY);
let computed_gt = parent_gt.mul_transform(local_transform);
let parent_iv = world
.get::<InheritedVisibility>(parent_entity)
.copied()
.unwrap_or(InheritedVisibility::VISIBLE);
let computed_iv = match local_visibility {
Visibility::Hidden => InheritedVisibility::HIDDEN,
Visibility::Visible => InheritedVisibility::VISIBLE,
Visibility::Inherited => parent_iv,
};
let entity = world
.spawn((
local_transform,
local_visibility,
computed_gt,
computed_iv,
ChildOf(parent_entity),
))
.id();
spawned[i] = entity;
for reflected in deferred {
world.entity_mut(entity).insert_reflect(reflected);
}
}
drop(registry_guard);
let gltf_entities: Vec<(Entity, String, usize)> = spawned
.iter()
.filter_map(|&e| {
world
.get::<jackdaw_jsn::GltfSource>(e)
.map(|gs| (e, gs.path.clone(), gs.scene_index))
})
.collect();
for (entity, gltf_path, scene_index) in gltf_entities {
let resolved = if Path::new(&gltf_path).is_relative() {
parent_path.join(&gltf_path).to_string_lossy().into_owned()
} else {
gltf_path
};
let label = format!("Scene{scene_index}");
let full_path = format!("{resolved}#{label}");
let scene_handle: Handle<Scene> = asset_server.load(full_path);
world.entity_mut(entity).insert(SceneRoot(scene_handle));
}
}
fn topological_index_order(entities: &[jackdaw_jsn::format::JsnEntity]) -> Vec<usize> {
let n = entities.len();
let mut order = Vec::with_capacity(n);
let mut emitted = vec![false; n];
for start in 0..n {
if emitted[start] {
continue;
}
let mut chain: Vec<usize> = Vec::new();
let mut cursor = start;
let mut steps = 0usize;
loop {
if emitted[cursor] {
break;
}
if steps > n {
warn!(
"Topological order: cycle detected at JSN entity index {cursor}; \
remaining chain treated as roots"
);
break;
}
steps += 1;
chain.push(cursor);
match entities[cursor].parent {
Some(p) if p < n && !emitted[p] => cursor = p,
_ => break,
}
}
for &i in chain.iter().rev() {
order.push(i);
emitted[i] = true;
}
}
order
}
fn load_inline_assets(
world: &mut World,
assets: &JsnAssets,
parent_path: &Path,
) -> HashMap<String, UntypedHandle> {
let mut local_assets: HashMap<String, UntypedHandle> = HashMap::new();
let linear_image_names = collect_linear_image_names(assets);
let registry = world.resource::<AppTypeRegistry>().clone();
let registry_guard = registry.read();
let asset_server = world.resource::<AssetServer>().clone();
for (type_path, named_entries) in &assets.0 {
for (name, json_value) in named_entries {
let serde_json::Value::String(rel_path) = json_value else {
continue;
};
if rel_path.starts_with('@') {
warn!(
"Catalog asset '{rel_path}' referenced by '{name}' is not supported at runtime"
);
continue;
}
let resolved = if Path::new(rel_path.as_str()).is_relative() {
parent_path.join(rel_path).to_string_lossy().into_owned()
} else {
rel_path.clone()
};
let handle = if type_path == "bevy_image::image::Image" {
if linear_image_names.contains(name) {
asset_server
.load_with_settings::<Image, ImageLoaderSettings>(
&resolved,
|s: &mut ImageLoaderSettings| s.is_srgb = false,
)
.untyped()
} else {
asset_server.load::<Image>(&resolved).untyped()
}
} else {
asset_server
.load::<bevy::asset::LoadedUntypedAsset>(&resolved)
.untyped()
};
local_assets.insert(name.clone(), handle);
}
}
for (type_path, named_entries) in &assets.0 {
let Some(registration) = registry_guard.get_with_type_path(type_path) else {
warn!("Unknown asset type '{type_path}' in inline assets; skipping");
continue;
};
let Some(reflect_asset) = registration.data::<ReflectAsset>() else {
continue;
};
for (name, json_value) in named_entries {
if json_value.is_string() {
continue;
}
let mut processor = RuntimeDeserializerProcessor {
asset_server: &asset_server,
parent_path,
local_assets: &local_assets,
entity_map: &[],
};
let deserializer = TypedReflectDeserializer::with_processor(
registration,
®istry_guard,
&mut processor,
);
let Ok(reflected) = deserializer.deserialize(json_value) else {
warn!("Failed to deserialize inline asset '{name}' of type '{type_path}'");
continue;
};
let handle = reflect_asset.add(world, reflected.as_ref());
local_assets.insert(name.clone(), handle);
}
}
local_assets
}
fn collect_linear_image_names(assets: &JsnAssets) -> HashSet<String> {
const LINEAR_SLOTS: &[&str] = &[
"normal_map_texture",
"metallic_roughness_texture",
"occlusion_texture",
"depth_map",
];
let mut linear_names = HashSet::new();
if let Some(materials) = assets.0.get("bevy_pbr::pbr_material::StandardMaterial") {
for json_value in materials.values() {
if let serde_json::Value::Object(obj) = json_value {
for slot in LINEAR_SLOTS {
if let Some(serde_json::Value::String(img_name)) = obj.get(*slot) {
linear_names.insert(img_name.clone());
}
}
}
}
}
linear_names
}
struct RuntimeDeserializerProcessor<'a> {
asset_server: &'a AssetServer,
parent_path: &'a Path,
local_assets: &'a HashMap<String, UntypedHandle>,
entity_map: &'a [Entity],
}
impl ReflectDeserializerProcessor for RuntimeDeserializerProcessor<'_> {
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
_registry: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
if registration.type_id() == TypeId::of::<f32>() {
let val = deserializer
.deserialize_any(F32Visitor)
.map_err(<D::Error as serde::de::Error>::custom)?;
return Ok(Ok(Box::new(val).into_partial_reflect()));
}
if registration.type_id() == TypeId::of::<f64>() {
let val = deserializer
.deserialize_any(F64Visitor)
.map_err(<D::Error as serde::de::Error>::custom)?;
return Ok(Ok(Box::new(val).into_partial_reflect()));
}
if registration.data::<ReflectHandle>().is_some() {
let path_str = deserializer.deserialize_any(StringOrNullVisitor)?;
if path_str.is_empty()
&& let Some(rd) = registration.data::<ReflectDefault>()
{
return Ok(Ok(rd.default().into_partial_reflect()));
}
if path_str.starts_with('@') {
warn!("Catalog asset '{path_str}' is not supported at runtime -- using default");
if let Some(rd) = registration.data::<ReflectDefault>() {
return Ok(Ok(rd.default().into_partial_reflect()));
}
}
if let Some(handle) = self.local_assets.get(&path_str) {
return Ok(Ok(Box::new(handle.clone()).into_partial_reflect()));
}
let label_pos = path_str.find('#').unwrap_or(path_str.len());
let file_part = &path_str[..label_pos];
let label_part = &path_str[label_pos..];
let resolved = if Path::new(file_part).is_relative() && !file_part.is_empty() {
self.parent_path
.join(file_part)
.to_string_lossy()
.into_owned()
} else {
file_part.to_owned()
};
let handle = self
.asset_server
.load_untyped(format!("{resolved}{label_part}"));
return Ok(Ok(Box::new(handle).into_partial_reflect()));
}
if registration.type_id() == TypeId::of::<Entity>() {
let Ok(idx_str) = deserializer.deserialize_any(StringOrNullVisitor) else {
return Ok(Ok(Box::new(Entity::PLACEHOLDER).into_partial_reflect()));
};
let idx: usize = idx_str.parse().unwrap_or(usize::MAX);
let entity = self
.entity_map
.get(idx)
.copied()
.unwrap_or(Entity::PLACEHOLDER);
return Ok(Ok(Box::new(entity).into_partial_reflect()));
}
Ok(Err(deserializer))
}
}
struct StringOrNullVisitor;
impl Visitor<'_> for StringOrNullVisitor {
type Value = String;
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "a string, integer, or null")
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(String::new())
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(v.to_owned())
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(v.to_string())
}
}
struct F32Visitor;
impl Visitor<'_> for F32Visitor {
type Value = f32;
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "a number or float string (inf, -inf, NaN)")
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(v as f32)
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(v as f32)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(v as f32)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v {
"inf" | "Infinity" => Ok(f32::INFINITY),
"-inf" | "-Infinity" => Ok(f32::NEG_INFINITY),
"NaN" | "nan" => Ok(f32::NAN),
_ => Err(E::custom(format!("unexpected float string: {v}"))),
}
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(0.0)
}
}
struct F64Visitor;
impl Visitor<'_> for F64Visitor {
type Value = f64;
fn expecting(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "a number or float string (inf, -inf, NaN)")
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(v)
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(v as f64)
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(v as f64)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v {
"inf" | "Infinity" => Ok(f64::INFINITY),
"-inf" | "-Infinity" => Ok(f64::NEG_INFINITY),
"NaN" | "nan" => Ok(f64::NAN),
_ => Err(E::custom(format!("unexpected float string: {v}"))),
}
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(0.0)
}
}