use crate::ecs::EditorWorld;
use crate::gltf_fetch;
use crate::systems::polyhaven::Category;
use nightshade::ecs::prefab::resources::mesh_cache_clear;
use nightshade::prelude::*;
pub fn preload_browsers(editor_world: &mut EditorWorld) {
editor_world
.resources
.browsers
.sample_browser
.ensure_loaded();
editor_world
.resources
.browsers
.polyhaven_browser
.ensure_loaded(Category::Models);
editor_world
.resources
.browsers
.polyhaven_browser
.ensure_loaded(Category::Hdris);
}
pub struct PendingImport {
pub name: String,
pub state: PendingImportState,
}
pub enum PendingImportState {
#[cfg(not(target_arch = "wasm32"))]
Threaded(std::sync::mpsc::Receiver<Result<nightshade::ecs::prefab::GltfLoadResult, String>>),
#[cfg(target_arch = "wasm32")]
Ready(Result<nightshade::ecs::prefab::GltfLoadResult, String>),
}
pub fn handle_dropped_data(
editor_world: &mut EditorWorld,
world: &mut World,
name: &str,
data: &[u8],
) {
let lower = name.to_lowercase();
if lower.ends_with(".gltf") || lower.ends_with(".glb") {
load_gltf_bytes(editor_world, name, data);
} else if lower.ends_with(".json") || lower.ends_with(".nsmap") {
crate::systems::project_io::load_project_bytes_into_editor(editor_world, world, name, data);
} else if lower.ends_with(".hdr") {
load_hdr_skybox(world, data.to_vec());
world.resources.graphics.atmosphere = Atmosphere::Hdr;
editor_world.resources.loading.last_dropped_error = None;
crate::scene_writeback::sync_hdr_skybox(
&mut editor_world.resources.project,
Some(nightshade::ecs::scene::SceneHdrSkybox::Embedded {
data: data.to_vec(),
}),
);
crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
tracing::info!("Loaded HDR skybox '{}'", name);
} else if lower.ends_with(".bin")
|| lower.ends_with(".png")
|| lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
{
tracing::info!(
"Ignoring '{}' (drop the entire glTF folder, not just the asset)",
name
);
editor_world.resources.loading.last_dropped_error = None;
} else {
editor_world.resources.loading.last_dropped_error =
Some(format!("Unsupported file: {}", name));
tracing::warn!("dropped file '{}' has unsupported extension", name);
}
}
pub fn handle_dropped_files(
editor_world: &mut EditorWorld,
world: &mut World,
files: &[nightshade::ecs::input::resources::DroppedFile],
) {
let gltf_index = files
.iter()
.position(|file| file.name.to_lowercase().ends_with(".gltf"));
if let Some(index) = gltf_index {
let gltf_file = &files[index];
let parent = gltf_file
.name
.rsplit_once('/')
.map(|(parent, _)| parent.to_string())
.unwrap_or_default();
let mut resources = std::collections::HashMap::new();
for (other_index, file) in files.iter().enumerate() {
if other_index == index {
continue;
}
let key = if parent.is_empty() {
file.name.clone()
} else if let Some(rest) = file.name.strip_prefix(&format!("{}/", parent)) {
rest.to_string()
} else {
file.name.clone()
};
resources.insert(key, file.data.clone());
}
let name = gltf_file.name.clone();
#[cfg(not(target_arch = "wasm32"))]
{
let (sender, receiver) = std::sync::mpsc::channel();
let bytes_owned = gltf_file.data.clone();
let resources_owned = resources;
std::thread::spawn(move || {
let result = nightshade::ecs::prefab::import_gltf_with_resources(
&bytes_owned,
&resources_owned,
)
.map_err(|error| error.to_string());
let _ = sender.send(result);
});
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name,
state: PendingImportState::Threaded(receiver),
});
}
#[cfg(target_arch = "wasm32")]
{
let result =
nightshade::ecs::prefab::import_gltf_with_resources(&gltf_file.data, &resources)
.map_err(|error| error.to_string());
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name,
state: PendingImportState::Ready(result),
});
}
return;
}
for file in files {
handle_dropped_data(editor_world, world, &file.name, &file.data);
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn handle_dropped_path(
editor_world: &mut EditorWorld,
world: &mut World,
path: &std::path::Path,
) {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("dropped")
.to_string();
let lower = name.to_lowercase();
if lower.ends_with(".gltf") || lower.ends_with(".glb") {
load_gltf_path(editor_world, path);
} else if lower.ends_with(".json") || lower.ends_with(".nsmap") {
match std::fs::read(path) {
Ok(bytes) => crate::systems::project_io::load_project_bytes_into_editor(
editor_world,
world,
&name,
&bytes,
),
Err(error) => report_load_error(editor_world, &name, format!("read failed: {error}")),
}
} else if lower.ends_with(".hdr") {
load_hdr_skybox_from_path(world, path.to_path_buf());
world.resources.graphics.atmosphere = Atmosphere::Hdr;
editor_world.resources.loading.last_dropped_error = None;
if let Some(path_str) = path.to_str() {
crate::scene_writeback::sync_hdr_skybox(
&mut editor_world.resources.project,
Some(nightshade::ecs::scene::SceneHdrSkybox::Reference {
path: path_str.to_string(),
}),
);
}
crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
tracing::info!("Loaded HDR skybox '{}'", name);
} else {
editor_world.resources.loading.last_dropped_error =
Some(format!("Unsupported file: {}", name));
tracing::warn!("dropped file '{}' has unsupported extension", name);
}
}
pub fn load_gltf_with_resources(
editor_world: &mut EditorWorld,
pending: gltf_fetch::PendingGltfMulti,
) {
let gltf_fetch::PendingGltfMulti {
display_name,
gltf_bytes,
resources,
} = pending;
#[cfg(not(target_arch = "wasm32"))]
{
let (sender, receiver) = std::sync::mpsc::channel();
let bytes_owned = gltf_bytes;
let resources_owned = resources;
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
nightshade::ecs::prefab::import_gltf_with_resources(&bytes_owned, &resources_owned)
.map_err(|error| error.to_string())
}));
let result = match result {
Ok(value) => value,
Err(payload) => {
let message = if let Some(text) = payload.downcast_ref::<&str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"import panicked".to_string()
};
Err(format!("import panicked: {message}"))
}
};
let _ = sender.send(result);
});
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: display_name,
state: PendingImportState::Threaded(receiver),
});
}
#[cfg(target_arch = "wasm32")]
{
let result = nightshade::ecs::prefab::import_gltf_with_resources(&gltf_bytes, &resources)
.map_err(|error| error.to_string());
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: display_name,
state: PendingImportState::Ready(result),
});
}
}
pub fn load_gltf_bytes(editor_world: &mut EditorWorld, name: &str, bytes: &[u8]) {
#[cfg(not(target_arch = "wasm32"))]
{
let (sender, receiver) = std::sync::mpsc::channel();
let bytes_owned = bytes.to_vec();
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
nightshade::ecs::prefab::import_gltf_from_bytes(&bytes_owned)
.map_err(|error| error.to_string())
}));
let result = match result {
Ok(value) => value,
Err(payload) => {
let message = if let Some(text) = payload.downcast_ref::<&str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"import panicked".to_string()
};
Err(format!("import panicked: {message}"))
}
};
let _ = sender.send(result);
});
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: name.to_string(),
state: PendingImportState::Threaded(receiver),
});
}
#[cfg(target_arch = "wasm32")]
{
let result = nightshade::ecs::prefab::import_gltf_from_bytes(bytes)
.map_err(|error| error.to_string());
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: name.to_string(),
state: PendingImportState::Ready(result),
});
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_gltf_path(editor_world: &mut EditorWorld, path: &std::path::Path) {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("dropped")
.to_string();
load_gltf_path_named(editor_world, &name, path);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_gltf_path_named(editor_world: &mut EditorWorld, name: &str, path: &std::path::Path) {
let (sender, receiver) = std::sync::mpsc::channel();
let path_owned = path.to_path_buf();
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
nightshade::ecs::prefab::import_gltf_from_path(&path_owned)
.map_err(|error| error.to_string())
}));
let result = match result {
Ok(value) => value,
Err(payload) => {
let message = if let Some(text) = payload.downcast_ref::<&str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"import panicked".to_string()
};
Err(format!("import panicked: {message}"))
}
};
let _ = sender.send(result);
});
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: name.to_string(),
state: PendingImportState::Threaded(receiver),
});
}
pub fn poll_pending_imports(editor_world: &mut EditorWorld, world: &mut World) {
let pending = std::mem::take(&mut editor_world.resources.loading.pending_imports);
for pending in pending {
match pending.state {
#[cfg(not(target_arch = "wasm32"))]
PendingImportState::Threaded(receiver) => match receiver.try_recv() {
Ok(Ok(result)) => apply_gltf_result(editor_world, world, &pending.name, result),
Ok(Err(error)) => report_load_error(editor_world, &pending.name, error),
Err(std::sync::mpsc::TryRecvError::Empty) => {
editor_world
.resources
.loading
.pending_imports
.push(PendingImport {
name: pending.name,
state: PendingImportState::Threaded(receiver),
});
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
report_load_error(
editor_world,
&pending.name,
"import thread terminated".to_string(),
);
}
},
#[cfg(target_arch = "wasm32")]
PendingImportState::Ready(Ok(result)) => {
apply_gltf_result(editor_world, world, &pending.name, result);
}
#[cfg(target_arch = "wasm32")]
PendingImportState::Ready(Err(error)) => {
report_load_error(editor_world, &pending.name, error);
}
}
}
}
pub fn poll_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
let entries_signature = editor_world
.resources
.browsers
.sample_browser
.entries()
.map(|e| e.len() as u64)
.unwrap_or(0);
if entries_signature > 0
&& entries_signature != editor_world.resources.browsers.thumbnail_signature
{
editor_world.resources.browsers.thumbnail_signature = entries_signature;
editor_world
.resources
.browsers
.sample_browser
.queue_thumbnails(&editor_world.resources.browsers.thumbnail_queue);
}
#[cfg(not(target_arch = "wasm32"))]
editor_world
.resources
.browsers
.kenney_browser
.queue_thumbnails(&editor_world.resources.browsers.thumbnail_queue);
process_texture_queue(&editor_world.resources.browsers.thumbnail_queue, 4);
let completed = editor_world
.resources
.browsers
.thumbnail_queue
.lock()
.map(|mut q| q.take_completed())
.unwrap_or_default();
for loaded in completed {
if let Some(upload) = ui_upload_image(world, &loaded.rgba_data, loaded.width, loaded.height)
{
editor_world.resources.browsers.thumbnail_slots.insert(
loaded.name,
crate::ecs::ThumbnailSlot {
layer: upload.layer,
uv_min: upload.uv_min,
uv_max: upload.uv_max,
aspect: vec2(upload.content_width as f32, upload.content_height as f32),
},
);
}
}
}
pub fn poll_browsers(editor_world: &mut EditorWorld, world: &mut World) {
if let Some(pending) = editor_world
.resources
.browsers
.sample_browser
.take_pending_glb()
{
load_gltf_bytes(editor_world, &pending.display_name, &pending.bytes);
}
if let Some(pending) = editor_world
.resources
.browsers
.polyhaven_browser
.take_pending_hdr()
{
let hdr_bytes = pending.bytes.clone();
load_hdr_skybox(world, pending.bytes);
world.resources.graphics.atmosphere = Atmosphere::Hdr;
crate::scene_writeback::sync_hdr_skybox(
&mut editor_world.resources.project,
Some(nightshade::ecs::scene::SceneHdrSkybox::Embedded { data: hdr_bytes }),
);
crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
tracing::info!("Loaded Polyhaven HDRI '{}'", pending.display_name);
}
if let Some(pending) = editor_world
.resources
.browsers
.polyhaven_browser
.take_pending_gltf()
{
load_gltf_with_resources(editor_world, pending);
}
if let Some(pending) = editor_world
.resources
.browsers
.sample_browser
.take_pending_gltf()
{
load_gltf_with_resources(editor_world, pending);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(pending) = editor_world
.resources
.browsers
.sketchfab_browser
.take_pending_gltf()
{
load_gltf_with_resources(editor_world, pending);
}
#[cfg(not(target_arch = "wasm32"))]
{
let pending_kenney = editor_world
.resources
.browsers
.kenney_browser
.drain_pending_glbs();
for pending in pending_kenney {
load_gltf_path_named(editor_world, &pending.display_name, &pending.path);
}
}
}
pub fn update_top_progress_bar(editor_world: &EditorWorld, world: &mut World) {
let pipeline = &world.resources.loading.pipeline;
let pipeline_active = nightshade::ecs::loading::loading_pipeline_is_active(pipeline);
let pipeline_total = nightshade::ecs::loading::loading_pipeline_total(pipeline);
let pipeline_completed = nightshade::ecs::loading::loading_pipeline_completed(pipeline);
let imports_active = !editor_world.resources.loading.pending_imports.is_empty();
let sample_active = editor_world
.resources
.browsers
.sample_browser
.loading_status()
.is_some();
let polyhaven_active = editor_world
.resources
.browsers
.polyhaven_browser
.loading_status()
.is_some();
#[cfg(not(target_arch = "wasm32"))]
let kenney_active = editor_world.resources.browsers.kenney_browser.is_busy();
#[cfg(target_arch = "wasm32")]
let kenney_active = false;
#[cfg(not(target_arch = "wasm32"))]
let sketchfab_active = matches!(
editor_world.resources.browsers.sketchfab_browser.status(),
crate::systems::sketchfab::FetchStatus::Working(_)
);
#[cfg(target_arch = "wasm32")]
let sketchfab_active = false;
let any_active = pipeline_active
|| imports_active
|| sample_active
|| polyhaven_active
|| kenney_active
|| sketchfab_active;
let bar = &mut world.resources.retained_ui.top_progress_bar;
if !any_active {
bar.clear();
return;
}
if pipeline_active && pipeline_total > 0 {
bar.set((pipeline_completed as f32 / pipeline_total as f32).clamp(0.0, 1.0));
} else {
bar.set_indeterminate();
}
}
pub fn poll_fit_frames(editor_world: &mut EditorWorld, world: &mut World) {
let frames = editor_world.resources.loading.pending_fit_frames;
if frames == 0 {
return;
}
let next = frames - 1;
editor_world.resources.loading.pending_fit_frames = next;
if next == 0 {
nightshade::ecs::transform::systems::update_global_transforms_system(world);
let roots = std::mem::take(&mut editor_world.resources.loading.pending_fit_roots);
if editor_world.resources.camera.reset_camera_on_load {
if roots.is_empty() {
crate::systems::camera::frame_scene(editor_world, world);
} else {
crate::systems::camera::frame_entities(editor_world, world, &roots);
}
}
}
}
pub fn clear_loaded_model(editor_world: &mut EditorWorld, world: &mut World) {
let entities = std::mem::take(&mut editor_world.resources.loading.model_entities);
for entity in entities {
let descendants = nightshade::ecs::transform::queries::query_descendants(world, entity);
crate::scene_writeback::remove_entity(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
entity,
);
for descendant in descendants {
crate::scene_writeback::remove_entity(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
descendant,
);
}
despawn_recursive_immediate(world, entity);
}
mesh_cache_clear(&mut world.resources.assets.mesh_cache);
world.resources.mesh_render_state.request_full_rebuild();
crate::systems::selection::clear(editor_world);
crate::systems::picking::reset_cycle(editor_world);
}
pub fn clear_scene(editor_world: &mut EditorWorld, world: &mut World) {
clear_loaded_model(editor_world, world);
editor_world.resources.loading.current_model_name = None;
let dev_entities = std::mem::take(&mut editor_world.resources.loading.dev_tool_entities);
for entity in dev_entities {
let descendants = nightshade::ecs::transform::queries::query_descendants(world, entity);
crate::scene_writeback::remove_entity(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
entity,
);
for descendant in descendants {
crate::scene_writeback::remove_entity(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
descendant,
);
}
despawn_recursive_immediate(world, entity);
}
}
pub fn apply_gltf_result(
editor_world: &mut EditorWorld,
world: &mut World,
name: &str,
mut result: nightshade::ecs::prefab::GltfLoadResult,
) {
let suggested_exposure = result.suggested_exposure;
if editor_world.resources.loading.viewer_mode {
clear_loaded_model(editor_world, world);
}
nightshade::ecs::loading::queue_gltf_load(world, &mut result);
let animations = result.animations.clone();
let skins = result.skins.clone();
let prefabs = result.prefabs.clone();
let single = prefabs.len() == 1;
let base_name = name.trim_end_matches(".gltf").trim_end_matches(".glb");
let mut spawned_roots: Vec<Entity> = Vec::with_capacity(prefabs.len());
for (index, prefab) in prefabs.into_iter().enumerate() {
let cache_key = if single {
base_name.to_string()
} else {
format!("{base_name}#{index}")
};
let cached = nightshade::ecs::prefab::resources::CachedPrefab {
prefab: prefab.clone(),
animations: animations.clone(),
skins: skins.clone(),
source_path: None,
};
nightshade::ecs::prefab::resources::prefab_cache_insert(
&mut world.resources.assets.prefab_cache,
cache_key,
cached,
);
let entity = nightshade::ecs::prefab::spawn_prefab_with_skins(
world,
&prefab,
&animations,
&skins,
Vec3::zeros(),
);
if let Some(player) = world.core.get_animation_player_mut(entity)
&& !player.clips.is_empty()
{
player.play(0);
}
spawned_roots.push(entity);
editor_world.resources.loading.model_entities.push(entity);
}
for &root in &spawned_roots {
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
root,
);
}
if !spawned_roots.is_empty() {
let mut combined_scene = nightshade::ecs::scene::Scene::new("undo_capture");
let mut root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = Vec::new();
let mut root_parents: Vec<Option<nightshade::ecs::scene::AssetUuid>> = Vec::new();
for &root in &spawned_roots {
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
for entity in captured.scene.entities {
combined_scene.add_entity(entity);
}
root_uuids.extend(captured.root_uuids);
root_parents.extend(captured.root_parents);
}
nightshade::ecs::scene::embed_referenced_meshes(world, &mut combined_scene);
nightshade::ecs::scene::embed_referenced_textures(world, &mut combined_scene);
combined_scene.compute_spawn_order();
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(crate::undo::CapturedSubtree {
scene: combined_scene,
root_uuids,
root_parents,
}),
},
format!("Load {name}"),
);
}
editor_world.resources.loading.loaded = true;
editor_world.resources.loading.pending_fit_frames = 2;
editor_world.resources.loading.pending_fit_roots = spawned_roots;
editor_world.resources.loading.current_model_name = Some(name.to_string());
editor_world.resources.loading.last_dropped_error = None;
world.resources.graphics.color_grading.exposure = suggested_exposure;
world.resources.graphics.color_grading.preset = ColorGradingPreset::Custom;
tracing::info!("Loaded GLTF '{}'", name);
}
fn report_load_error(editor_world: &mut EditorWorld, name: &str, error: String) {
tracing::error!("Failed to load '{}': {}", name, error);
editor_world.resources.loading.last_dropped_error = Some(error);
}