use crate::ecs::EditorWorld;
use crate::systems::polyhaven::Category;
use crate::systems::retained_ui::camera_grid;
use crate::systems::retained_ui::viewport::ExtraCameraTile;
use crate::systems::retained_ui::viewport_controls;
use crate::systems::{camera, random};
use nightshade::ecs::camera::components::ShadingMode;
use nightshade::prelude::*;
#[derive(Default)]
pub struct UiInteraction {
pub actions: Vec<Action>,
}
#[derive(Debug, Clone)]
pub enum Action {
OpenKhronosBrowser,
OpenPolyhavenBrowser,
#[cfg(not(target_arch = "wasm32"))]
OpenSketchfabBrowser,
#[cfg(not(target_arch = "wasm32"))]
OpenKenneyBrowser,
#[cfg(not(target_arch = "wasm32"))]
PickKenneyDirectory,
#[cfg(not(target_arch = "wasm32"))]
PickKenneyZip,
#[cfg(not(target_arch = "wasm32"))]
LoadKenneyByIndex(usize),
OpenMaterialsBrowser,
OpenTagManager,
OpenPrefabBrowser,
ToggleTreePanel,
ToggleInspectorPanel,
ToggleGroundGrid,
ToggleSky,
ToggleLitUnlit,
ToggleViewerMode,
FrameScene,
ToggleFrameOnLoad,
ClearScene,
SnapSelectionToFloor,
BakeNavmesh,
ToggleNavmeshDebug,
SetAtmosphere(Atmosphere),
SetGizmoMode(nightshade::ecs::gizmos::GizmoMode),
RandomizeBoth,
SelectEntity(Entity),
ToggleSelectEntity(Entity),
ClearSelection,
AddEntityChild(Entity),
ViewCamera(Entity),
ShowAllCameras,
ResetViewportLayout,
ToggleIconSet,
ToggleSnap,
ToggleSkeletonView,
ToggleShowNormals,
OpenNormalsSettings,
OpenSnapSettings,
ToggleDayNightCycle,
LoadKhronosByIndex(usize),
LoadPolyhavenByIndex(Category, usize),
#[cfg(not(target_arch = "wasm32"))]
SketchfabFetch {
token: String,
url: String,
},
SpawnLines,
SpawnMeshes,
Spawn3DText,
SpawnTextLattice,
SetShadingMode(Entity, ShadingMode),
ToggleViewportOverlays(Entity),
ToggleViewportRealtime(Entity),
NewProject,
OpenProject,
SaveProject,
SaveProjectAs,
SaveSelectedAsPrefab,
ImportPrefab,
RefreshSelectedPrefab,
BreakSelectedPrefabLink,
Undo,
Redo,
DeleteEntity(Entity),
DeleteSelectedEntity,
DuplicateEntity(Entity),
DuplicateSelectedEntity,
AddCube,
AddSphere,
AddCylinder,
AddCone,
AddTorus,
AddPlane,
AddInstancedCube,
AddInstancedSphere,
AddInstancedCylinder,
AddInstancedCone,
AddInstancedTorus,
AddInstancedPlane,
ConvertSimilarToInstanced,
AddEmpty,
AddPlayerSpawn,
AddSavePoint,
AddPatrolPoint,
AddTriggerVolume,
StampDecalAtCursor,
LoadHdrSkybox,
AddPointLight,
AddSpotLight,
AddDirectionalLight,
AddTagToSelected(String),
RemoveTagFromSelected(String),
OpenCommandPalette,
}
pub fn drain(editor_world: &mut EditorWorld, world: &mut World) {
let actions = std::mem::take(&mut editor_world.resources.ui_interaction.actions);
for action in actions {
apply(editor_world, world, action);
}
}
fn apply(editor_world: &mut EditorWorld, world: &mut World, action: Action) {
match action {
Action::OpenKhronosBrowser => {
editor_world
.resources
.browsers
.sample_browser
.ensure_loaded();
let panel = editor_world.resources.ui_handles.browsers.khronos.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
Action::OpenPolyhavenBrowser => {
editor_world
.resources
.browsers
.polyhaven_browser
.ensure_loaded(Category::Hdris);
editor_world
.resources
.browsers
.polyhaven_browser
.ensure_loaded(Category::Models);
let panel = editor_world.resources.ui_handles.browsers.polyhaven.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
#[cfg(not(target_arch = "wasm32"))]
Action::OpenSketchfabBrowser => {
let panel = editor_world.resources.ui_handles.browsers.sketchfab.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
#[cfg(not(target_arch = "wasm32"))]
Action::OpenKenneyBrowser => {
let panel = editor_world.resources.ui_handles.browsers.kenney.panel;
ui_set_visible(world, panel, true);
}
#[cfg(not(target_arch = "wasm32"))]
Action::PickKenneyDirectory => {
editor_world
.resources
.browsers
.kenney_browser
.pick_directory();
}
#[cfg(not(target_arch = "wasm32"))]
Action::PickKenneyZip => {
editor_world.resources.browsers.kenney_browser.pick_zip();
}
#[cfg(not(target_arch = "wasm32"))]
Action::LoadKenneyByIndex(index) => {
let entry = editor_world
.resources
.browsers
.kenney_browser
.entries()
.and_then(|entries| entries.get(index).cloned());
if let Some(entry) = entry {
crate::systems::kenney_assets::load_entry(
&editor_world.resources.browsers.kenney_browser,
&entry,
);
}
}
Action::OpenMaterialsBrowser => {
let panel = editor_world.resources.ui_handles.materials.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
Action::OpenTagManager => {
let panel = editor_world.resources.ui_handles.tag_manager.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
Action::OpenPrefabBrowser => {
let panel = editor_world.resources.ui_handles.prefab_browser.panel;
ui_set_visible(world, panel, !ui_node_visible(world, panel));
}
Action::ToggleTreePanel => {
editor_world.resources.ui.show_tree = !editor_world.resources.ui.show_tree;
let panel = editor_world.resources.ui_handles.tree.panel;
let visible = editor_world.resources.ui.show_tree;
ui_set_visible(world, panel, visible);
ui_context_menu_set_checked(
world,
editor_world.resources.ui_handles.top_bar.view_menu,
0,
visible,
);
}
Action::ToggleInspectorPanel => {
editor_world.resources.ui.show_inspector = !editor_world.resources.ui.show_inspector;
let panel = editor_world.resources.ui_handles.inspector.panel;
let visible = editor_world.resources.ui.show_inspector;
ui_set_visible(world, panel, visible);
ui_context_menu_set_checked(
world,
editor_world.resources.ui_handles.top_bar.view_menu,
1,
visible,
);
}
Action::ToggleGroundGrid => {
world.resources.graphics.show_grid = !world.resources.graphics.show_grid;
}
Action::ToggleSky => {
world.resources.graphics.show_sky = !world.resources.graphics.show_sky;
}
Action::ToggleLitUnlit => {
world.resources.graphics.unlit_mode = !world.resources.graphics.unlit_mode;
}
Action::ToggleViewerMode => {
editor_world.resources.loading.viewer_mode =
!editor_world.resources.loading.viewer_mode;
}
Action::FrameScene => camera::frame_scene(editor_world, world),
Action::ClearScene => {
let captured = capture_active_scene_for_undo(editor_world, world);
crate::systems::loading::clear_scene(editor_world, world);
if let Some(captured) = captured {
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityDeleted {
captured: Box::new(captured),
},
"Clear scene",
);
}
}
Action::ToggleFrameOnLoad => {
editor_world.resources.camera.reset_camera_on_load =
!editor_world.resources.camera.reset_camera_on_load;
}
Action::SetAtmosphere(atmosphere) => {
world.resources.graphics.atmosphere = atmosphere;
}
Action::SnapSelectionToFloor => {
snap_selection_to_floor(editor_world, world);
}
Action::BakeNavmesh => {
let config = nightshade::ecs::navmesh::generation::RecastNavMeshConfig::default();
nightshade::ecs::navmesh::generation::generate_navmesh_from_world(world, &config);
}
Action::ToggleNavmeshDebug => {
world.resources.navmesh.debug_draw = !world.resources.navmesh.debug_draw;
}
Action::SetGizmoMode(mode) => {
world.resources.user_interface.gizmos.mode = mode;
}
Action::RandomizeBoth => random::request_both(editor_world),
Action::ShowAllCameras => {
let container = editor_world.resources.ui_handles.viewport.tile_container;
let existing_tiles: Vec<(TileId, Entity, Entity)> = editor_world
.resources
.ui_handles
.viewport
.extra_camera_tiles
.iter()
.map(|tile| (tile.pane_id, tile.controls.container, tile.content_entity))
.collect();
for (tile_id, controls_container, content_entity) in existing_tiles {
ui_set_visible(world, controls_container, false);
ui_set_visible(world, content_entity, false);
ui_tile_remove(world, container, tile_id);
}
editor_world
.resources
.ui_handles
.viewport
.extra_camera_tiles
.clear();
let active_camera = world.resources.active_camera;
const MAX_VISIBLE_CAMERA_TILES: usize = 8;
let mut camera_entities: Vec<Entity> = Vec::new();
world
.core
.query()
.with(nightshade::ecs::world::CAMERA)
.iter(|entity, _, _| camera_entities.push(entity));
let extras: Vec<(Entity, String)> = camera_entities
.iter()
.copied()
.filter(|camera| Some(*camera) != active_camera)
.take(MAX_VISIBLE_CAMERA_TILES)
.enumerate()
.map(|(index, camera)| {
let title = world
.core
.get_name(camera)
.map(|name| name.0.clone())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| format!("Camera {}", index + 2));
(camera, title)
})
.collect();
let main_pane = editor_world.resources.ui_handles.viewport.viewport_pane;
let main_content = editor_world.resources.ui_handles.viewport.viewport_content;
let titles: Vec<String> = extras.iter().map(|(_, title)| title.clone()).collect();
let Some(layout) =
camera_grid::rebuild_grid(world, container, main_pane, main_content, &titles)
else {
return;
};
editor_world.resources.ui_handles.viewport.viewport_pane = layout.main_pane;
let mut new_tiles: Vec<ExtraCameraTile> = Vec::with_capacity(layout.extras.len());
for ((pane_id, content_entity), (camera, _title)) in
layout.extras.into_iter().zip(extras)
{
super::viewport::configure_viewport_pane(world, content_entity);
viewport_controls::ensure_shading_with_mode(world, camera, ShadingMode::Solid);
if !world
.core
.entity_has_components(camera, nightshade::ecs::world::VIEWPORT_UPDATE_MODE)
{
world
.core
.add_components(camera, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
}
world.core.set_viewport_update_mode(
camera,
nightshade::ecs::camera::components::ViewportUpdateMode::WhenDirty,
);
let mut tree = UiTreeBuilder::new(world);
let controls = viewport_controls::build(&mut tree, camera);
tree.finish();
new_tiles.push(ExtraCameraTile {
pane_id,
camera_entity: camera,
content_entity,
controls,
});
}
editor_world
.resources
.ui_handles
.viewport
.extra_camera_tiles
.extend(new_tiles);
}
Action::ToggleIconSet => {
let new_set = match world.resources.retained_ui.icon_set {
IconSet::Material => IconSet::Lucide,
IconSet::Lucide => IconSet::Material,
};
world.resources.retained_ui.icon_set = new_set;
let lookup = IconLookup::for_set(new_set);
for (entity, icon_fn) in &editor_world.resources.ui_handles.top_bar.icon_entities {
ui_set_icon(world, *entity, icon_fn(lookup));
}
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
Action::ToggleDayNightCycle => {
editor_world.resources.sun.auto_cycle = !editor_world.resources.sun.auto_cycle;
ui_context_menu_set_checked(
world,
editor_world.resources.ui_handles.top_bar.view_menu,
4,
editor_world.resources.sun.auto_cycle,
);
}
Action::ToggleSnap => {
editor_world.resources.snap.enabled = !editor_world.resources.snap.enabled;
}
Action::ToggleSkeletonView => {
let enabled = &mut editor_world.resources.skeleton_debug.enabled;
*enabled = !*enabled;
ui_context_menu_set_checked(
world,
editor_world.resources.ui_handles.top_bar.view_menu,
7,
*enabled,
);
}
Action::OpenSnapSettings => {
let panel = editor_world.resources.ui_handles.snap_settings.panel;
ui_set_visible(world, panel, true);
}
Action::ToggleShowNormals => {
let enabled = !world.resources.graphics.show_normals;
world.resources.graphics.show_normals = enabled;
ui_context_menu_set_checked(
world,
editor_world.resources.ui_handles.top_bar.view_menu,
8,
enabled,
);
}
Action::OpenNormalsSettings => {
let panel = editor_world.resources.ui_handles.normals_settings.panel;
ui_set_visible(world, panel, true);
}
Action::ResetViewportLayout => {
let container = editor_world.resources.ui_handles.viewport.tile_container;
let viewport_content = editor_world.resources.ui_handles.viewport.viewport_content;
let notes_content = editor_world.resources.ui_handles.viewport.notes_content;
for tile in &editor_world
.resources
.ui_handles
.viewport
.extra_camera_tiles
{
ui_set_visible(world, tile.controls.container, false);
ui_set_visible(world, tile.content_entity, false);
}
if let Some(new_panes) = ui_tile_reset_to_panes(
world,
container,
&[(viewport_content, "Viewport"), (notes_content, "Notes")],
) {
editor_world.resources.ui_handles.viewport.viewport_pane = new_panes[0];
editor_world.resources.ui_handles.viewport.notes_pane = new_panes[1];
}
editor_world
.resources
.ui_handles
.viewport
.extra_camera_tiles
.clear();
}
Action::SelectEntity(entity) => {
crate::systems::selection::set_primary(editor_world, Some(entity));
crate::systems::picking::reset_cycle(editor_world);
}
Action::ToggleSelectEntity(entity) => {
crate::systems::selection::toggle(editor_world, entity);
crate::systems::picking::reset_cycle(editor_world);
}
Action::ClearSelection => {
crate::systems::selection::clear(editor_world);
crate::systems::picking::reset_cycle(editor_world);
}
Action::ViewCamera(camera_entity) => {
if world.core.entity_has_camera(camera_entity) {
world.resources.active_camera = Some(camera_entity);
}
}
Action::AddEntityChild(parent) => {
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | PARENT,
1,
)[0];
world.core.set_parent(entity, Parent(Some(parent)));
world
.core
.set_name(entity, Name(format!("Entity {}", entity.id)));
world.core.set_local_transform(
entity,
LocalTransform {
translation: Vec3::zeros(),
rotation: Quat::identity(),
scale: Vec3::new(1.0, 1.0, 1.0),
},
);
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
world
.core
.set_global_transform(entity, GlobalTransform::default());
editor_world
.resources
.ui_handles
.tree
.expanded
.insert(parent.id);
crate::systems::selection::set_primary(editor_world, Some(entity));
editor_world.resources.ui_handles.tree.last_signature = 0;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
crate::systems::picking::reset_cycle(editor_world);
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured = crate::undo::capture_subtree(
world,
&mut editor_world.resources.editor_scene,
entity,
);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add entity",
);
}
Action::LoadKhronosByIndex(index) => {
let entry = editor_world
.resources
.browsers
.sample_browser
.entries()
.and_then(|entries| entries.get(index).cloned());
if let Some(entry) = entry {
editor_world
.resources
.browsers
.sample_browser
.fetch_entry(&entry);
}
}
Action::LoadPolyhavenByIndex(category, index) => {
let entry = editor_world
.resources
.browsers
.polyhaven_browser
.entries(category)
.and_then(|entries| entries.get(index).cloned());
if let Some(entry) = entry {
editor_world
.resources
.browsers
.polyhaven_browser
.fetch_asset(category, &entry.slug, &entry.name);
}
}
#[cfg(not(target_arch = "wasm32"))]
Action::SketchfabFetch { token, url } => {
let browser = &mut editor_world.resources.browsers.sketchfab_browser;
browser.token = token;
browser.url = url;
if browser.can_fetch() {
browser.start_fetch();
}
}
Action::SpawnLines => {
let entity = crate::dev_tools::spawn_lines(world);
register_dev_tool_root(editor_world, world, entity);
}
Action::SpawnMeshes => {
let entity = crate::dev_tools::spawn_meshes(world);
register_dev_tool_root(editor_world, world, entity);
}
Action::Spawn3DText => {
let entity = crate::dev_tools::spawn_3d_text(world);
register_dev_tool_root(editor_world, world, entity);
}
Action::SpawnTextLattice => {
let entity = crate::dev_tools::spawn_text_lattice(world);
register_dev_tool_root(editor_world, world, entity);
}
Action::SetShadingMode(camera_entity, mode) => {
viewport_controls::ensure_shading(world, camera_entity);
if let Some(shading) = world.core.get_viewport_shading_mut(camera_entity) {
shading.mode = mode;
}
}
Action::ToggleViewportOverlays(camera_entity) => {
viewport_controls::ensure_shading(world, camera_entity);
if let Some(shading) = world.core.get_viewport_shading_mut(camera_entity) {
shading.show_overlays = !shading.show_overlays;
}
}
Action::ToggleViewportRealtime(camera_entity) => {
if !world
.core
.entity_has_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE)
{
world
.core
.add_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
world.core.set_viewport_update_mode(
camera_entity,
nightshade::ecs::camera::components::ViewportUpdateMode::Always,
);
}
if let Some(mode) = world.core.get_viewport_update_mode_mut(camera_entity) {
*mode = match *mode {
nightshade::ecs::camera::components::ViewportUpdateMode::Always => {
nightshade::ecs::camera::components::ViewportUpdateMode::WhenDirty
}
_ => nightshade::ecs::camera::components::ViewportUpdateMode::Always,
};
}
}
Action::NewProject => {
crate::systems::project_io::new_project(editor_world, world);
}
Action::OpenProject => {
crate::systems::project_io::open_project(editor_world);
}
Action::SaveProject => {
crate::systems::project_io::save_project(editor_world, world);
}
Action::SaveProjectAs => {
crate::systems::project_io::save_project_as(editor_world, world);
}
Action::SaveSelectedAsPrefab => {
crate::systems::project_io::save_selected_as_prefab(editor_world, world);
}
Action::ImportPrefab => {
crate::systems::project_io::import_prefab(editor_world);
}
Action::RefreshSelectedPrefab => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
crate::systems::retained_ui::prefab_browser::refresh_selected_instance(
editor_world,
world,
entity,
);
}
}
Action::BreakSelectedPrefabLink => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
crate::systems::retained_ui::prefab_browser::break_instance_link(
editor_world,
world,
entity,
);
}
}
Action::Undo => {
apply_history_action(editor_world, world, true);
}
Action::Redo => {
apply_history_action(editor_world, world, false);
}
Action::DeleteEntity(entity) => {
delete_entity(editor_world, world, entity);
}
Action::DeleteSelectedEntity => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
delete_entity(editor_world, world, entity);
}
}
Action::DuplicateEntity(entity) => {
duplicate_entity(editor_world, world, entity);
}
Action::DuplicateSelectedEntity => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
duplicate_entity(editor_world, world, entity);
}
}
Action::AddCube => {
spawn_primitive(editor_world, world, PrimitiveKind::Cube);
}
Action::AddSphere => {
spawn_primitive(editor_world, world, PrimitiveKind::Sphere);
}
Action::AddCylinder => {
spawn_primitive(editor_world, world, PrimitiveKind::Cylinder);
}
Action::AddCone => {
spawn_primitive(editor_world, world, PrimitiveKind::Cone);
}
Action::AddTorus => {
spawn_primitive(editor_world, world, PrimitiveKind::Torus);
}
Action::AddPlane => {
spawn_primitive(editor_world, world, PrimitiveKind::Plane);
}
Action::AddInstancedCube => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cube);
}
Action::AddInstancedSphere => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Sphere);
}
Action::AddInstancedCylinder => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cylinder);
}
Action::AddInstancedCone => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cone);
}
Action::AddInstancedTorus => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Torus);
}
Action::AddInstancedPlane => {
spawn_instanced_primitive(editor_world, world, PrimitiveKind::Plane);
}
Action::ConvertSimilarToInstanced => {
convert_similar_to_instanced(editor_world, world);
}
Action::AddEmpty => {
spawn_empty_entity(editor_world, world);
}
Action::AddPlayerSpawn => {
spawn_tagged_marker(editor_world, world, "Player Spawn", "player_spawn");
}
Action::AddSavePoint => {
spawn_tagged_marker(editor_world, world, "Save Point", "save_point");
}
Action::AddPatrolPoint => {
spawn_tagged_marker(editor_world, world, "Patrol Point", "patrol_point");
}
Action::AddTriggerVolume => {
spawn_trigger_volume(editor_world, world);
}
Action::StampDecalAtCursor => {
stamp_decal_at_cursor(editor_world, world);
}
Action::LoadHdrSkybox => {
load_hdr_skybox_from_picker(editor_world, world);
}
Action::AddPointLight => {
spawn_light_entity(editor_world, world, LightType::Point);
}
Action::AddSpotLight => {
spawn_light_entity(editor_world, world, LightType::Spot);
}
Action::AddDirectionalLight => {
spawn_light_entity(editor_world, world, LightType::Directional);
}
Action::AddTagToSelected(tag) => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
add_tag(editor_world, world, entity, tag);
}
}
Action::RemoveTagFromSelected(tag) => {
if let Some(entity) = editor_world.resources.ui.selected_entity {
remove_tag(editor_world, world, entity, &tag);
}
}
Action::OpenCommandPalette => {
let palette = editor_world.resources.ui_handles.command_palette.entity;
ui_show_command_palette(world, palette);
}
}
}
#[derive(Clone, Copy)]
enum PrimitiveKind {
Cube,
Sphere,
Cylinder,
Cone,
Torus,
Plane,
}
fn primitive_mesh_name(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Cube => "Cube",
PrimitiveKind::Sphere => "Sphere",
PrimitiveKind::Cylinder => "Cylinder",
PrimitiveKind::Cone => "Cone",
PrimitiveKind::Torus => "Torus",
PrimitiveKind::Plane => "Plane",
}
}
fn spawn_primitive(editor_world: &mut EditorWorld, world: &mut World, kind: PrimitiveKind) {
let position = camera::focus_point(editor_world, world);
let entity = match kind {
PrimitiveKind::Cube => spawn_cube_at(world, position),
PrimitiveKind::Sphere => spawn_sphere_at(world, position),
PrimitiveKind::Cylinder => spawn_cylinder_at(world, position),
PrimitiveKind::Cone => spawn_cone_at(world, position),
PrimitiveKind::Torus => spawn_torus_at(world, position),
PrimitiveKind::Plane => spawn_plane_at(world, position),
};
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add primitive",
);
select_new_entity(editor_world, entity);
}
fn ensure_primitive_mesh_cached(world: &mut World, mesh_name: &str) {
use nightshade::ecs::mesh::components::{
create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
create_sphere_mesh, create_torus_mesh,
};
if world
.resources
.assets
.mesh_cache
.registry
.name_to_index
.contains_key(mesh_name)
{
return;
}
let mesh = match mesh_name {
"Cube" => Some(create_cube_mesh()),
"Sphere" => Some(create_sphere_mesh(1.0, 16)),
"Plane" => Some(create_plane_mesh(2.0)),
"Torus" => Some(create_torus_mesh(1.0, 0.3, 32, 16)),
"Cylinder" => Some(create_cylinder_mesh(0.5, 1.0, 16)),
"Cone" => Some(create_cone_mesh(0.5, 1.0, 16)),
_ => None,
};
if let Some(mesh) = mesh {
mesh_cache_insert(
&mut world.resources.assets.mesh_cache,
mesh_name.to_string(),
mesh,
);
}
}
fn spawn_instanced_primitive(
editor_world: &mut EditorWorld,
world: &mut World,
kind: PrimitiveKind,
) {
let mesh_name = primitive_mesh_name(kind);
ensure_primitive_mesh_cached(world, mesh_name);
let position = camera::focus_point(editor_world, world);
let instances = vec![nightshade::ecs::mesh::components::InstanceTransform::default()];
let entity = spawn_instanced_mesh_with_material(world, mesh_name, instances, "Default");
world
.core
.set_name(entity, Name(format!("Instanced {mesh_name}")));
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.translation = position;
}
mark_local_transform_dirty(world, entity);
world
.core
.set_bounding_volume(entity, BoundingVolume::from_mesh_type(mesh_name));
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add instanced primitive",
);
select_new_entity(editor_world, entity);
}
fn decompose_global_transform(
matrix: &Mat4,
) -> nightshade::ecs::mesh::components::InstanceTransform {
let translation = Vec3::new(matrix[(0, 3)], matrix[(1, 3)], matrix[(2, 3)]);
let column_x = Vec3::new(matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)]);
let column_y = Vec3::new(matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)]);
let column_z = Vec3::new(matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]);
let scale = Vec3::new(column_x.norm(), column_y.norm(), column_z.norm());
let safe_scale = Vec3::new(
if scale.x.abs() < 1.0e-6 { 1.0 } else { scale.x },
if scale.y.abs() < 1.0e-6 { 1.0 } else { scale.y },
if scale.z.abs() < 1.0e-6 { 1.0 } else { scale.z },
);
let rotation_basis = nalgebra_glm::Mat3::new(
column_x.x / safe_scale.x,
column_y.x / safe_scale.y,
column_z.x / safe_scale.z,
column_x.y / safe_scale.x,
column_y.y / safe_scale.y,
column_z.y / safe_scale.z,
column_x.z / safe_scale.x,
column_y.z / safe_scale.y,
column_z.z / safe_scale.z,
);
let rotation = nalgebra_glm::mat3_to_quat(&rotation_basis);
nightshade::ecs::mesh::components::InstanceTransform::new(translation, rotation, scale)
}
fn convert_similar_to_instanced(editor_world: &mut EditorWorld, world: &mut World) {
let Some(selected) = editor_world.resources.ui.selected_entity else {
return;
};
let Some(mesh) = world.core.get_render_mesh(selected).cloned() else {
return;
};
let mesh_name = mesh.name.clone();
let selected_material_name = world
.core
.get_material_ref(selected)
.map(|reference| reference.name.clone());
let candidate_entities: Vec<Entity> = world
.core
.query_entities(
nightshade::ecs::world::RENDER_MESH | nightshade::ecs::world::GLOBAL_TRANSFORM,
)
.collect();
let mut matches: Vec<Entity> = Vec::new();
for entity in candidate_entities {
if editor_world.resources.editor_scene.is_scaffolding(entity) {
continue;
}
let Some(render_mesh) = world.core.get_render_mesh(entity) else {
continue;
};
if render_mesh.name != mesh_name {
continue;
}
let entity_material = world
.core
.get_material_ref(entity)
.map(|reference| reference.name.clone());
if entity_material != selected_material_name {
continue;
}
if !nightshade::ecs::transform::queries::query_descendants(world, entity).is_empty() {
continue;
}
matches.push(entity);
}
if matches.len() < 2 {
return;
}
let mut instances: Vec<nightshade::ecs::mesh::components::InstanceTransform> =
Vec::with_capacity(matches.len());
for entity in &matches {
let Some(global) = world.core.get_global_transform(*entity).copied() else {
continue;
};
instances.push(decompose_global_transform(&global.0));
}
if instances.is_empty() {
return;
}
let material_name = selected_material_name.unwrap_or_else(|| "Default".to_string());
ensure_primitive_mesh_cached(world, &mesh_name);
let new_entity =
spawn_instanced_mesh_with_material(world, &mesh_name, instances, &material_name);
world
.core
.set_name(new_entity, Name(format!("Instanced {mesh_name}")));
world
.core
.set_bounding_volume(new_entity, BoundingVolume::from_mesh_type(&mesh_name));
let captured_originals = capture_entities_for_undo(editor_world, world, &matches);
for entity in &matches {
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);
}
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
new_entity,
);
let new_captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, new_entity);
let mut steps: Vec<crate::undo::UndoableOperation> = Vec::with_capacity(2);
if let Some(captured_originals) = captured_originals {
steps.push(crate::undo::UndoableOperation::EntityDeleted {
captured: Box::new(captured_originals),
});
}
steps.push(crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(new_captured),
});
editor_world.resources.undo.push(
crate::undo::UndoableOperation::Batch { steps },
"Convert to instanced",
);
select_new_entity(editor_world, new_entity);
}
fn capture_entities_for_undo(
editor_world: &mut EditorWorld,
world: &World,
entities: &[Entity],
) -> Option<crate::undo::CapturedSubtree> {
let mut combined = nightshade::ecs::scene::Scene::new("convert_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 entity in entities {
if !world.core.entity_has_local_transform(*entity) {
continue;
}
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, *entity);
for scene_entity in captured.scene.entities {
combined.add_entity(scene_entity);
}
root_uuids.extend(captured.root_uuids);
root_parents.extend(captured.root_parents);
}
if root_uuids.is_empty() {
return None;
}
combined.compute_spawn_order();
Some(crate::undo::CapturedSubtree {
scene: combined,
root_uuids,
root_parents,
})
}
fn snap_selection_to_floor(editor_world: &mut EditorWorld, world: &mut World) {
let selected: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
if selected.is_empty() {
return;
}
let mut undo_steps: Vec<crate::undo::UndoableOperation> = Vec::new();
for entity in selected {
let Some(global) = world.core.get_global_transform(entity).copied() else {
continue;
};
let Some(local) = world.core.get_local_transform(entity).copied() else {
continue;
};
let world_position = global.translation();
let origin = Vec3::new(world_position.x, world_position.y + 0.01, world_position.z);
let direction = Vec3::new(0.0, -1.0, 0.0);
let hit = nightshade::ecs::physics::resources::physics_world_cast_ray(
&world.resources.physics,
origin,
direction,
10_000.0,
Some(entity),
);
let target_y = hit.map(|hit| hit.point.y).unwrap_or(0.0);
let delta_y_world = target_y - world_position.y;
if delta_y_world.abs() < 1e-6 {
continue;
}
let new_local_translation = Vec3::new(
local.translation.x,
local.translation.y + delta_y_world,
local.translation.z,
);
let new_local = LocalTransform {
translation: new_local_translation,
rotation: local.rotation,
scale: local.scale,
};
let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity) else {
continue;
};
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.translation = new_local_translation;
}
mark_local_transform_dirty(world, entity);
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
undo_steps.push(crate::undo::UndoableOperation::TransformChanged {
uuid,
old: local,
new: new_local,
});
}
if undo_steps.is_empty() {
return;
}
if undo_steps.len() == 1 {
editor_world
.resources
.undo
.push(undo_steps.into_iter().next().unwrap(), "Snap to floor");
} else {
editor_world.resources.undo.push(
crate::undo::UndoableOperation::Batch { steps: undo_steps },
"Snap to floor",
);
}
editor_world.resources.ui_handles.inspector.dirty = true;
}
#[cfg(not(target_arch = "wasm32"))]
fn load_hdr_skybox_from_picker(editor_world: &mut EditorWorld, world: &mut World) {
let picked = nightshade::prelude::rfd::FileDialog::new()
.set_title("Select HDR skybox")
.add_filter("HDR image", &["hdr", "exr"])
.pick_file();
let Some(path) = picked else { return };
nightshade::ecs::world::commands::queue_render_command(
world,
nightshade::ecs::world::commands::RenderCommand::LoadHdrSkyboxFromPath {
path: path.clone(),
},
);
crate::scene_writeback::sync_hdr_skybox(
&mut editor_world.resources.project,
Some(nightshade::ecs::scene::SceneHdrSkybox::Reference {
path: path.to_string_lossy().to_string(),
}),
);
world.resources.graphics.atmosphere = nightshade::prelude::Atmosphere::Hdr;
}
#[cfg(target_arch = "wasm32")]
fn load_hdr_skybox_from_picker(_editor_world: &mut EditorWorld, _world: &mut World) {}
fn stamp_decal_at_cursor(editor_world: &mut EditorWorld, world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let mouse = nightshade::ecs::input::access::mouse_for_active(world);
let mouse_position = mouse.position;
let viewport = match world
.resources
.window
.camera_tile_rects
.get(&camera_entity)
.copied()
.or(world.resources.window.active_viewport_rect)
{
Some(rect) if rect.width > 0.0 && rect.height > 0.0 => rect,
_ => return,
};
if !viewport.contains(mouse_position) {
return;
}
let Some(camera) = world.core.get_camera(camera_entity).copied() else {
return;
};
let Some(camera_transform) = world.core.get_global_transform(camera_entity) else {
return;
};
let camera_position = camera_transform.translation();
let view_matrix = camera_transform
.0
.try_inverse()
.unwrap_or_else(Mat4::identity);
let aspect = viewport.width / viewport.height;
let view_projection = camera.projection.matrix_with_aspect(aspect) * view_matrix;
let inverse_view_projection = view_projection.try_inverse().unwrap_or_else(Mat4::identity);
let ndc_x = ((mouse_position.x - viewport.x) / viewport.width) * 2.0 - 1.0;
let ndc_y = 1.0 - ((mouse_position.y - viewport.y) / viewport.height) * 2.0;
let near_clip = inverse_view_projection * nalgebra_glm::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
if near_clip.w.abs() < 1e-6 {
return;
}
let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
let direction = (near_world - camera_position).normalize();
let hit = nightshade::ecs::physics::resources::physics_world_cast_ray(
&world.resources.physics,
camera_position,
direction,
10_000.0,
None,
);
let (hit_point, hit_normal) = match hit {
Some(hit) => (hit.point, hit.normal),
None => {
let denominator = direction.y;
if denominator.abs() < 1e-4 {
return;
}
let parameter = -camera_position.y / denominator;
if parameter <= 0.0 {
return;
}
(
camera_position + direction * parameter,
Vec3::new(0.0, 1.0, 0.0),
)
}
};
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | DECAL,
1,
)[0];
world
.core
.set_name(entity, Name(format!("Decal {}", entity.id)));
let projection_direction = -hit_normal;
let up_reference = if projection_direction.y.abs() > 0.95 {
Vec3::new(0.0, 0.0, 1.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let rotation = nalgebra_glm::quat_look_at_rh(&projection_direction, &up_reference);
world.core.set_local_transform(
entity,
LocalTransform {
translation: hit_point + hit_normal * 0.01,
rotation,
scale: Vec3::new(1.0, 1.0, 1.0),
},
);
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
world
.core
.set_global_transform(entity, GlobalTransform::default());
world
.core
.set_decal(entity, nightshade::ecs::decal::components::Decal::default());
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Stamp decal",
);
select_new_entity(editor_world, entity);
}
fn spawn_tagged_marker(editor_world: &mut EditorWorld, world: &mut World, name: &str, tag: &str) {
let position = camera::focus_point(editor_world, world);
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
1,
)[0];
world
.core
.set_name(entity, Name(format!("{name} {}", entity.id)));
world.core.set_local_transform(
entity,
LocalTransform {
translation: position,
rotation: Quat::identity(),
scale: Vec3::new(0.25, 0.25, 0.25),
},
);
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
world
.core
.set_global_transform(entity, GlobalTransform::default());
world
.resources
.entities
.tags
.entry(entity)
.or_default()
.push(tag.to_string());
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
format!("Add {name}"),
);
select_new_entity(editor_world, entity);
}
fn spawn_trigger_volume(editor_world: &mut EditorWorld, world: &mut World) {
let position = camera::focus_point(editor_world, world);
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | COLLIDER,
1,
)[0];
world
.core
.set_name(entity, Name(format!("Trigger Volume {}", entity.id)));
world.core.set_local_transform(
entity,
LocalTransform {
translation: position,
rotation: Quat::identity(),
scale: Vec3::new(1.0, 1.0, 1.0),
},
);
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
world
.core
.set_global_transform(entity, GlobalTransform::default());
let mut collider = ColliderComponent::new_cuboid(1.0, 1.0, 1.0);
collider.is_sensor = true;
world.core.set_collider(entity, collider);
world
.core
.add_components(entity, nightshade::ecs::world::COLLISION_LISTENER);
world.core.set_collision_listener(
entity,
nightshade::ecs::physics::components::CollisionListener,
);
world
.resources
.entities
.tags
.entry(entity)
.or_default()
.push("trigger".to_string());
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add trigger volume",
);
select_new_entity(editor_world, entity);
}
fn spawn_empty_entity(editor_world: &mut EditorWorld, world: &mut World) {
let position = camera::focus_point(editor_world, world);
let entity = spawn_entities(
world,
NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
1,
)[0];
world
.core
.set_name(entity, Name(format!("Empty {}", entity.id)));
world.core.set_local_transform(
entity,
LocalTransform {
translation: position,
rotation: Quat::identity(),
scale: Vec3::new(1.0, 1.0, 1.0),
},
);
world
.core
.set_local_transform_dirty(entity, LocalTransformDirty);
world
.core
.set_global_transform(entity, GlobalTransform::default());
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add empty",
);
select_new_entity(editor_world, entity);
}
fn spawn_light_entity(editor_world: &mut EditorWorld, world: &mut World, light_type: LightType) {
let position = camera::focus_point(editor_world, world) + Vec3::new(0.0, 2.0, 0.0);
let color = Vec3::new(1.0, 0.95, 0.8);
let entity = spawn_sphere_at(world, position);
let (name, intensity, range, material_prefix) = match light_type {
LightType::Point => ("Point Light", 5.0_f32, 10.0_f32, "PointLightMaterial"),
LightType::Spot => ("Spot Light", 10.0_f32, 15.0_f32, "SpotLightMaterial"),
LightType::Directional => (
"Directional Light",
3.0_f32,
0.0_f32,
"DirectionalLightMaterial",
),
};
world.core.set_name(entity, Name(name.to_string()));
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.scale = Vec3::new(0.15, 0.15, 0.15);
}
mark_local_transform_dirty(world, entity);
world.core.add_components(entity, LIGHT);
world.core.set_light(
entity,
Light {
light_type,
color,
intensity,
range,
..Default::default()
},
);
let material_name = format!("{material_prefix}_{}", entity.id);
spawn_material(
world,
entity,
material_name,
nightshade::ecs::material::components::Material {
base_color: [color.x, color.y, color.z, 1.0],
emissive_factor: [color.x, color.y, color.z],
emissive_strength: 4.0,
roughness: 1.0,
metallic: 0.0,
unlit: false,
..Default::default()
},
);
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Add light",
);
select_new_entity(editor_world, entity);
}
fn delete_entity(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
if editor_world.resources.editor_scene.is_scaffolding(entity) {
return;
}
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
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);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityDeleted {
captured: Box::new(captured),
},
"Delete entity",
);
crate::systems::selection::remove(editor_world, entity);
crate::systems::picking::reset_cycle(editor_world);
editor_world.resources.ui_handles.tree.last_signature = 0;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}
fn duplicate_entity(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
if editor_world.resources.editor_scene.is_scaffolding(entity) {
return;
}
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
let mut clone_scene = nightshade::ecs::scene::Scene::new("duplicate");
let mut uuid_remap: std::collections::HashMap<
nightshade::ecs::scene::AssetUuid,
nightshade::ecs::scene::AssetUuid,
> = std::collections::HashMap::new();
for source in &captured.scene.entities {
let new_uuid = nightshade::ecs::scene::AssetUuid::random();
uuid_remap.insert(source.uuid, new_uuid);
}
for source in &captured.scene.entities {
let mut clone = source.clone();
clone.uuid = uuid_remap[&source.uuid];
clone.parent = source
.parent
.and_then(|parent| uuid_remap.get(&parent).copied());
if uuid_remap.contains_key(&source.uuid) && captured.root_uuids.contains(&source.uuid) {
clone.transform.translation += Vec3::new(0.5, 0.0, 0.5);
}
clone_scene.add_entity(clone);
}
clone_scene.compute_spawn_order();
let new_root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = captured
.root_uuids
.iter()
.filter_map(|uuid| uuid_remap.get(uuid).copied())
.collect();
let result = match nightshade::ecs::scene::spawn_scene(world, &clone_scene, None) {
Ok(result) => result,
Err(error) => {
tracing::error!("duplicate failed: {error}");
return;
}
};
for (uuid, runtime_entity) in &result.uuid_to_entity {
editor_world
.resources
.editor_scene
.insert(*uuid, *runtime_entity);
}
let parent_uuid = world
.core
.get_parent(entity)
.and_then(|p| p.0)
.and_then(|parent| editor_world.resources.editor_scene.uuid_for(parent));
if let Some(parent_uuid) = parent_uuid {
for root_uuid in &new_root_uuids {
let Some(child) = result.uuid_to_entity.get(root_uuid).copied() else {
continue;
};
if let Some(parent) = editor_world.resources.editor_scene.entity_for(parent_uuid) {
world.core.set_parent(
child,
nightshade::ecs::transform::components::Parent(Some(parent)),
);
}
}
}
for runtime_entity in result.uuid_to_entity.values() {
crate::scene_writeback::add_entity(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
*runtime_entity,
);
}
let new_capture = crate::undo::CapturedSubtree {
scene: clone_scene,
root_uuids: new_root_uuids.clone(),
root_parents: vec![parent_uuid; new_root_uuids.len()],
};
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(new_capture),
},
"Duplicate",
);
if let Some(first_uuid) = new_root_uuids.first()
&& let Some(new_entity) = result.uuid_to_entity.get(first_uuid)
{
select_new_entity(editor_world, *new_entity);
}
}
fn add_tag(editor_world: &mut EditorWorld, world: &mut World, entity: Entity, tag: String) {
if tag.is_empty() {
return;
}
let tags = world.resources.entities.tags.entry(entity).or_default();
if !tags.contains(&tag) {
tags.push(tag);
}
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
editor_world.resources.ui_handles.inspector.dirty = true;
}
fn remove_tag(editor_world: &mut EditorWorld, world: &mut World, entity: Entity, tag: &str) {
if let Some(tags) = world.resources.entities.tags.get_mut(&entity) {
tags.retain(|existing| existing != tag);
if tags.is_empty() {
world.resources.entities.tags.remove(&entity);
}
}
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
editor_world.resources.ui_handles.inspector.dirty = true;
}
fn select_new_entity(editor_world: &mut EditorWorld, entity: Entity) {
crate::systems::selection::set_primary(editor_world, Some(entity));
editor_world.resources.ui_handles.tree.last_signature = 0;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
crate::systems::picking::reset_cycle(editor_world);
}
fn apply_history_action(editor_world: &mut EditorWorld, world: &mut World, is_undo: bool) {
crate::systems::picking::reset_cycle(editor_world);
let mut history = std::mem::take(&mut editor_world.resources.undo);
let result = {
let mut ctx = crate::undo::UndoCtx {
world,
editor_scene: &mut editor_world.resources.editor_scene,
project: &mut editor_world.resources.project,
};
if is_undo {
history.undo(&mut ctx)
} else {
history.redo(&mut ctx)
}
};
editor_world.resources.undo = history;
if let Some(result) = result {
if let Some(entity) = result.select_entity {
crate::systems::selection::set_primary(editor_world, Some(entity));
}
editor_world.resources.ui_handles.tree.last_signature = 0;
editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
editor_world.resources.writeback_state.mark_full_resync();
}
}
fn capture_active_scene_for_undo(
editor_world: &mut EditorWorld,
world: &World,
) -> Option<crate::undo::CapturedSubtree> {
let model_roots = editor_world.resources.loading.model_entities.clone();
let dev_roots = editor_world.resources.loading.dev_tool_entities.clone();
let mut roots: Vec<Entity> = Vec::with_capacity(model_roots.len() + dev_roots.len());
roots.extend(model_roots);
roots.extend(dev_roots);
if roots.is_empty() {
return None;
}
let mut combined = 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 roots {
if !world.core.entity_has_local_transform(root) {
continue;
}
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
for entity in captured.scene.entities {
combined.add_entity(entity);
}
root_uuids.extend(captured.root_uuids);
root_parents.extend(captured.root_parents);
}
if root_uuids.is_empty() {
return None;
}
combined.compute_spawn_order();
Some(crate::undo::CapturedSubtree {
scene: combined,
root_uuids,
root_parents,
})
}
fn register_dev_tool_root(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
editor_world
.resources
.loading
.dev_tool_entities
.push(entity);
crate::scene_writeback::register_subtree(
&mut editor_world.resources.project,
&mut editor_world.resources.editor_scene,
world,
entity,
);
let captured =
crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
editor_world.resources.undo.push(
crate::undo::UndoableOperation::EntityCreated {
captured: Box::new(captured),
},
"Spawn",
);
}