use std::path::Path;
use bevy::{
ecs::system::{SystemParam, SystemState},
gltf::GltfAssetLabel,
prelude::*,
};
use crate::{
EditorEntity,
commands::{CommandHistory, DespawnEntity, EditorCommand},
selection::{Selected, Selection},
};
use bevy::input_focus::InputFocus;
#[derive(Resource)]
pub struct SystemClipboard {
clipboard: arboard::Clipboard,
last_jsn: String,
}
impl Default for SystemClipboard {
fn default() -> Self {
Self {
clipboard: arboard::Clipboard::new().expect("Failed to init system clipboard"),
last_jsn: String::new(),
}
}
}
pub use jackdaw_jsn::GltfSource;
pub struct EntityOpsPlugin;
impl Plugin for EntityOpsPlugin {
fn build(&self, app: &mut App) {
match arboard::Clipboard::new() {
Ok(clipboard) => {
app.insert_resource(SystemClipboard {
clipboard,
last_jsn: String::new(),
});
}
Err(e) => {
warn!("Failed to initialize system clipboard: {e}");
}
}
app.register_type::<EmptyEntity>()
.register_type::<SceneCamera>()
.register_type::<SceneLight>();
}
}
#[derive(Component, Default, Reflect)]
#[reflect(Component, @crate::EditorHidden)]
pub struct EmptyEntity;
#[derive(Component, Default, Reflect)]
#[reflect(Component, @crate::EditorHidden)]
pub struct SceneCamera;
#[derive(Component, Default, Reflect)]
#[reflect(Component, @crate::EditorHidden)]
pub struct SceneLight;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntityTemplate {
Empty,
Cube,
Sphere,
PointLight,
DirectionalLight,
SpotLight,
Camera3d,
}
impl EntityTemplate {
pub fn label(self) -> &'static str {
match self {
Self::Empty => "Empty Entity",
Self::Cube => "Cube",
Self::Sphere => "Sphere",
Self::PointLight => "Point Light",
Self::DirectionalLight => "Directional Light",
Self::SpotLight => "Spot Light",
Self::Camera3d => "Camera",
}
}
}
pub fn create_entity(
commands: &mut Commands,
template: EntityTemplate,
selection: &mut Selection,
) -> Entity {
let entity = match template {
EntityTemplate::Empty => commands
.spawn((
Name::new("Empty"),
EmptyEntity,
Transform::default(),
Visibility::default(),
))
.id(),
EntityTemplate::Cube => {
let id = commands
.spawn((
Name::new("Cube"),
crate::brush::Brush::cuboid(0.5, 0.5, 0.5),
Transform::default(),
Visibility::default(),
))
.id();
commands.queue(apply_last_material(id));
id
}
EntityTemplate::Sphere => {
let id = commands
.spawn((
Name::new("Sphere"),
crate::brush::Brush::sphere(0.5),
Transform::default(),
Visibility::default(),
))
.id();
commands.queue(apply_last_material(id));
id
}
EntityTemplate::PointLight => commands
.spawn((
Name::new("Point Light"),
SceneLight,
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(0.0, 3.0, 0.0),
))
.id(),
EntityTemplate::DirectionalLight => commands
.spawn((
Name::new("Directional Light"),
SceneLight,
DirectionalLight {
shadows_enabled: true,
..default()
},
Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.8, 0.4, 0.0)),
))
.id(),
EntityTemplate::SpotLight => commands
.spawn((
Name::new("Spot Light"),
SceneLight,
SpotLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(0.0, 3.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
))
.id(),
EntityTemplate::Camera3d => commands
.spawn((
Name::new("Camera"),
SceneCamera,
Camera3d::default(),
Camera {
is_active: false,
..default()
},
bevy::camera::RenderTarget::None {
size: UVec2::splat(1),
},
Transform::from_xyz(0.0, 2.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
))
.id(),
};
selection.select_single(commands, entity);
entity
}
fn apply_last_material(entity: Entity) -> impl FnOnce(&mut World) {
move |world: &mut World| {
let last_mat = world
.resource::<crate::brush::LastUsedMaterial>()
.material
.clone();
if let Some(mat) = last_mat
&& let Some(mut brush) = world.get_mut::<crate::brush::Brush>(entity)
{
for face in &mut brush.faces {
face.material = mat.clone();
}
}
}
}
pub fn create_entity_in_world(world: &mut World, template: EntityTemplate) {
let label = format!("Add {}", template.label());
let spawn_fn = Box::new(move |world: &mut World| -> Entity {
let mut system_state: SystemState<(Commands, ResMut<Selection>)> = SystemState::new(world);
let (mut commands, mut selection) = system_state.get_mut(world);
let entity = create_entity(&mut commands, template, &mut selection);
system_state.apply(world);
crate::scene_io::register_entity_in_ast(world, entity);
entity
});
let mut cmd: Box<dyn EditorCommand> = Box::new(crate::commands::SpawnEntity {
spawned: None,
spawn_fn,
label,
});
cmd.execute(world);
world.resource_mut::<CommandHistory>().push_executed(cmd);
}
pub fn spawn_gltf(
commands: &mut Commands,
asset_server: &AssetServer,
path: &str,
position: Vec3,
selection: &mut Selection,
) -> Entity {
let file_name = Path::new(path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "GLTF Model".to_string());
let scene_index = 0;
let asset_path = to_asset_path(path);
let scene = asset_server.load(GltfAssetLabel::Scene(scene_index).from_asset(asset_path));
let entity = commands
.spawn((
Name::new(file_name),
GltfSource {
path: path.to_string(),
scene_index,
},
SceneRoot(scene),
Transform::from_translation(position),
))
.id();
selection.select_single(commands, entity);
entity
}
pub fn spawn_gltf_in_world(world: &mut World, path: &str, position: Vec3) {
let mut system_state: SystemState<(Commands, Res<AssetServer>, ResMut<Selection>)> =
SystemState::new(world);
let (mut commands, asset_server, mut selection) = system_state.get_mut(world);
spawn_gltf(&mut commands, &asset_server, path, position, &mut selection);
system_state.apply(world);
}
pub fn delete_selected(world: &mut World) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
for &entity in &entities {
if world.get_entity(entity).is_err() {
continue;
}
if world.get::<EditorEntity>(entity).is_some() {
continue;
}
cmds.push(Box::new(DespawnEntity::from_world(world, entity)));
}
for &entity in &entities {
if let Ok(mut ec) = world.get_entity_mut(entity) {
ec.remove::<Selected>();
}
}
let mut selection = world.resource_mut::<Selection>();
selection.entities.clear();
for cmd in &mut cmds {
cmd.execute(world);
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Delete entities".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
pub fn duplicate_selected(world: &mut World) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
for &entity in &entities {
if let Ok(mut ec) = world.get_entity_mut(entity) {
ec.remove::<Selected>();
}
}
let mut new_entities = Vec::new();
for &entity in &entities {
if world.get_entity(entity).is_err() {
continue;
}
if world.get::<EditorEntity>(entity).is_some() {
continue;
}
let mut snapshot_entities = Vec::new();
crate::commands::collect_entity_ids(world, entity, &mut snapshot_entities);
let scene = DynamicSceneBuilder::from_world(world)
.extract_entities(snapshot_entities.into_iter())
.build();
let mut entity_map = Default::default();
if scene.write_to_world(world, &mut entity_map).is_err() {
continue;
}
let Some(&new_root) = entity_map.get(&entity) else {
continue;
};
if let Some(name) = world.get::<Name>(new_root) {
let mut base = name.as_str().to_string();
while base.ends_with(" (Copy)") {
base.truncate(base.len() - 7);
}
if let Some(pos) = base.rfind(' ')
&& base[pos + 1..].parse::<u32>().is_ok()
{
base.truncate(pos);
}
let mut max_num = 0u32;
let mut query = world.query::<&Name>();
for existing in query.iter(world) {
let s = existing.as_str();
if s == base {
max_num = max_num.max(1);
} else if let Some(rest) = s.strip_prefix(base.as_str())
&& let Some(num_str) = rest.strip_prefix(' ')
&& let Ok(n) = num_str.parse::<u32>()
{
max_num = max_num.max(n);
}
}
let new_name = format!("{} {}", base, max_num + 1);
world.entity_mut(new_root).insert(Name::new(new_name));
}
let parent = world.get::<ChildOf>(entity).map(|c| c.0);
if let Some(parent) = parent {
world.entity_mut(new_root).insert(ChildOf(parent));
} else {
world.entity_mut(new_root).remove::<ChildOf>();
}
new_entities.push(new_root);
}
crate::scene_io::register_entities_in_ast(world, &new_entities);
let mut selection = world.resource_mut::<Selection>();
selection.entities = new_entities;
for &entity in &selection.entities.clone() {
world.entity_mut(entity).insert(Selected);
}
}
fn snap_to_nearest_axis(v: Vec3) -> Vec3 {
let abs = v.abs();
if abs.x >= abs.y && abs.x >= abs.z {
Vec3::new(v.x.signum(), 0.0, 0.0)
} else if abs.y >= abs.x && abs.y >= abs.z {
Vec3::new(0.0, v.y.signum(), 0.0)
} else {
Vec3::new(0.0, 0.0, v.z.signum())
}
}
pub(crate) fn camera_snapped_rotation_axes(gt: &GlobalTransform) -> (Vec3, Vec3, Vec3) {
let yaw_axis = Vec3::Y;
let fwd = gt.forward().as_vec3();
let fwd_horiz = Vec3::new(fwd.x, 0.0, fwd.z);
let roll_axis = if fwd_horiz.length_squared() > 1e-6 {
-snap_to_nearest_axis(fwd_horiz)
} else {
let up = gt.up().as_vec3();
let up_horiz = Vec3::new(up.x, 0.0, up.z);
if up_horiz.length_squared() > 1e-6 {
snap_to_nearest_axis(up_horiz)
} else {
Vec3::NEG_Z
}
};
let right = gt.right().as_vec3();
let mut pitch_axis = snap_to_nearest_axis(right);
if pitch_axis.abs() == roll_axis.abs() {
pitch_axis = snap_to_nearest_axis(yaw_axis.cross(roll_axis));
}
(yaw_axis, roll_axis, pitch_axis)
}
pub(crate) enum TransformReset {
Position,
Rotation,
Scale,
}
pub(crate) fn reset_transform_selected(world: &mut World, reset: TransformReset) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
for &entity in &entities {
if world.get_entity(entity).is_err() {
continue;
}
let Some(&old_transform) = world.get::<Transform>(entity) else {
continue;
};
let new_transform = match reset {
TransformReset::Position => Transform {
translation: Vec3::ZERO,
..old_transform
},
TransformReset::Rotation => Transform {
rotation: Quat::IDENTITY,
..old_transform
},
TransformReset::Scale => Transform {
scale: Vec3::ONE,
..old_transform
},
};
if old_transform == new_transform {
continue;
}
let mut cmd = crate::commands::SetTransform {
entity,
old_transform,
new_transform,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let label = match reset {
TransformReset::Position => "Reset position",
TransformReset::Rotation => "Reset rotation",
TransformReset::Scale => "Reset scale",
};
let group = crate::commands::CommandGroup {
commands: cmds,
label: label.to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
pub(crate) fn nudge_selected(world: &mut World, offset: Vec3) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
for &entity in &entities {
if world.get_entity(entity).is_err() {
continue;
}
let Some(&old_transform) = world.get::<Transform>(entity) else {
continue;
};
let new_transform = Transform {
translation: old_transform.translation + offset,
..old_transform
};
let mut cmd = crate::commands::SetTransform {
entity,
old_transform,
new_transform,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Nudge".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
pub(crate) fn rotate_selected(world: &mut World, rotation: Quat) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
for &entity in &entities {
if world.get_entity(entity).is_err() {
continue;
}
let Some(&old_transform) = world.get::<Transform>(entity) else {
continue;
};
let new_transform = Transform {
rotation: rotation * old_transform.rotation,
..old_transform
};
let mut cmd = crate::commands::SetTransform {
entity,
old_transform,
new_transform,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Rotate 90\u{00b0}".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
fn copy_components(world: &mut World) {
let selection = world.resource::<Selection>();
if selection.entities.is_empty() {
return;
}
let ast = world.resource::<jackdaw_jsn::SceneJsnAst>();
let jsn_entities: Vec<jackdaw_jsn::format::JsnEntity> = selection
.entities
.iter()
.filter_map(|&e| {
ast.node_for_entity(e)
.map(|node| jackdaw_jsn::format::JsnEntity {
parent: None,
components: node.components.clone(),
})
})
.collect();
if jsn_entities.is_empty() {
warn!("Copy: no selected entities have AST nodes");
return;
}
let jsn_text = match serde_json::to_string_pretty(&jsn_entities) {
Ok(t) => t,
Err(e) => {
warn!("Failed to serialize entities for clipboard: {e}");
return;
}
};
let Some(mut cb) = world.get_resource_mut::<SystemClipboard>() else {
return;
};
info!(
"Display: WAYLAND_DISPLAY={:?} DISPLAY={:?}",
std::env::var("WAYLAND_DISPLAY").ok(),
std::env::var("DISPLAY").ok(),
);
cb.last_jsn = jsn_text.clone();
match cb.clipboard.set_text(&jsn_text) {
Ok(()) => {
match cb.clipboard.get_text() {
Ok(readback) => info!(
"Clipboard set+readback OK ({} bytes written, {} read back)",
jsn_text.len(),
readback.len(),
),
Err(e) => warn!("Clipboard set OK but readback failed: {e}"),
}
}
Err(e) => warn!("Copy: system clipboard failed ({e}), using internal fallback"),
}
}
fn paste_components(world: &mut World) {
let jsn_text = {
let Some(mut cb) = world.get_resource_mut::<SystemClipboard>() else {
return;
};
cb.clipboard
.get_text()
.unwrap_or_else(|_| cb.last_jsn.clone())
};
if jsn_text.trim().is_empty() {
return;
}
let parsed: Vec<jackdaw_jsn::format::JsnEntity> = match serde_json::from_str(&jsn_text) {
Ok(entities) => entities,
Err(e) => {
warn!("Clipboard text is not valid JSN: {e}");
return;
}
};
if parsed.is_empty() {
return;
}
let local_assets = std::collections::HashMap::new();
let parent_path = std::path::Path::new(".");
let spawned = crate::scene_io::load_scene_from_jsn(world, &parsed, parent_path, &local_assets);
crate::scene_io::register_entities_in_ast(world, &spawned);
for &entity in &world.resource::<Selection>().entities.clone() {
if let Ok(mut ec) = world.get_entity_mut(entity) {
ec.remove::<Selected>();
}
}
let mut selection = world.resource_mut::<Selection>();
selection.entities = spawned.clone();
for &entity in &spawned {
world.entity_mut(entity).insert(Selected);
}
info!("Pasted {} entities from JSN clipboard", spawned.len());
}
fn hide_selected(world: &mut World) {
let selection = world.resource::<Selection>();
let entities: Vec<Entity> = selection.entities.clone();
if entities.is_empty() {
return;
}
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
for &entity in &entities {
let current = world
.get::<Visibility>(entity)
.copied()
.unwrap_or(Visibility::Inherited);
let new_visibility = match current {
Visibility::Hidden => Visibility::Inherited,
_ => Visibility::Hidden,
};
let mut cmd = crate::commands::SetJsnField {
entity,
type_path: "bevy_camera::visibility::Visibility".to_string(),
field_path: String::new(),
old_value: serde_json::Value::String(format!("{current:?}")),
new_value: serde_json::Value::String(format!("{new_visibility:?}")),
was_derived: false,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Toggle visibility".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
#[derive(SystemParam, Deref, DerefMut)]
struct SceneEntities<'w, 's> {
query: Query<
'w,
's,
(Entity, &'static Visibility),
(With<Name>, Without<EditorEntity>, Without<Node>),
>,
}
fn unhide_all_entities(world: &mut World, scene_entities: &mut SystemState<SceneEntities>) {
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
let hidden: Vec<Entity> = {
scene_entities
.get(world)
.iter()
.filter(|(_, vis)| **vis == Visibility::Hidden)
.map(|(e, _)| e)
.collect()
};
for entity in hidden {
let mut cmd = crate::commands::SetJsnField {
entity,
type_path: "bevy_camera::visibility::Visibility".to_string(),
field_path: String::new(),
old_value: serde_json::Value::String("Hidden".to_string()),
new_value: serde_json::Value::String("Inherited".to_string()),
was_derived: false,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Unhide all".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
fn hide_all_entities(world: &mut World, scene_entities: &mut SystemState<SceneEntities>) {
let mut cmds: Vec<Box<dyn EditorCommand>> = Vec::new();
let to_hide: Vec<(Entity, Visibility)> = {
scene_entities
.get(world)
.iter()
.filter(|(_, vis)| **vis != Visibility::Hidden)
.map(|(e, vis)| (e, *vis))
.collect()
};
for (entity, current) in to_hide {
let mut cmd = crate::commands::SetJsnField {
entity,
type_path: "bevy_camera::visibility::Visibility".to_string(),
field_path: String::new(),
old_value: serde_json::Value::String(format!("{current:?}")),
new_value: serde_json::Value::String("Hidden".to_string()),
was_derived: false,
};
cmd.execute(world);
cmds.push(Box::new(cmd));
}
if !cmds.is_empty() {
let group = crate::commands::CommandGroup {
commands: cmds,
label: "Hide all".to_string(),
};
let mut history = world.resource_mut::<CommandHistory>();
history.push_executed(Box::new(group));
}
}
fn to_asset_path(path: &str) -> String {
let path = Path::new(path);
if let Some(assets_dir) = get_assets_base_dir()
&& let Ok(relative) = path.strip_prefix(&assets_dir)
{
return relative.to_string_lossy().to_string();
}
if !path.is_absolute() {
return path.to_string_lossy().to_string();
}
warn!(
"Cannot load '{}': file is outside the assets directory. \
Move it into your project's assets/ folder.",
path.display()
);
path.to_string_lossy().to_string()
}
fn get_assets_base_dir() -> Option<std::path::PathBuf> {
if let Some(project_dir) = crate::project::read_last_project() {
let assets = project_dir.join("assets");
if assets.is_dir() {
return Some(assets);
}
}
let base = if let Ok(dir) = std::env::var("BEVY_ASSET_ROOT") {
std::path::PathBuf::from(dir)
} else if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
std::path::PathBuf::from(dir)
} else {
std::env::current_exe().ok()?.parent()?.to_path_buf()
};
Some(base.join("assets"))
}
use bevy_enhanced_input::prelude::{Press, *};
use jackdaw_api::prelude::*;
use crate::core_extension::CoreExtensionInputContext;
pub(crate) fn add_to_extension(ctx: &mut ExtensionContext) {
ctx.register_operator::<EntityDeleteOp>()
.register_operator::<EntityDuplicateOp>()
.register_operator::<EntityCopyComponentsOp>()
.register_operator::<EntityPasteComponentsOp>()
.register_operator::<EntityToggleVisibilityOp>()
.register_operator::<EntityHideUnselectedOp>()
.register_operator::<EntityUnhideAllOp>()
.register_operator::<EntityAddCubeOp>()
.register_operator::<EntityAddSphereOp>()
.register_operator::<EntityAddPointLightOp>()
.register_operator::<EntityAddDirectionalLightOp>()
.register_operator::<EntityAddSpotLightOp>()
.register_operator::<EntityAddCameraOp>()
.register_operator::<EntityAddEmptyOp>()
.register_operator::<EntityAddNavmeshOp>()
.register_operator::<EntityAddTerrainOp>()
.register_operator::<EntityAddPrefabOp>();
let ext = ctx.id();
ctx.entity_mut().world_scope(|world| {
world.spawn((
Action::<EntityDeleteOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(KeyCode::Delete, Press::default())],
));
world.spawn((
Action::<EntityDuplicateOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(
KeyCode::KeyD.with_mod_keys(ModKeys::CONTROL),
Press::default(),
)],
));
world.spawn((
Action::<EntityCopyComponentsOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(
KeyCode::KeyC.with_mod_keys(ModKeys::CONTROL),
Press::default(),
)],
));
world.spawn((
Action::<EntityPasteComponentsOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(
KeyCode::KeyV.with_mod_keys(ModKeys::CONTROL),
Press::default(),
)],
));
world.spawn((
Action::<EntityToggleVisibilityOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(KeyCode::KeyH, Press::default())],
));
world.spawn((
Action::<EntityUnhideAllOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(
KeyCode::KeyH.with_mod_keys(ModKeys::CONTROL),
Press::default(),
)],
));
world.spawn((
Action::<EntityHideUnselectedOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(KeyCode::KeyH.with_mod_keys(ModKeys::ALT), Press::default(),)],
));
});
}
fn can_act_on_entities(
input_focus: Res<InputFocus>,
active: ActiveModalQuery,
modal: Res<crate::modal_transform::ModalTransformState>,
draw_state: Res<crate::draw_brush::DrawBrushState>,
edit_mode: Res<crate::brush::EditMode>,
) -> bool {
if input_focus.0.is_some() || active.is_modal_running() || modal.active.is_some() {
return false;
}
if draw_state.active.is_some() {
return false;
}
matches!(*edit_mode, crate::brush::EditMode::Object)
}
#[operator(
id = "entity.delete",
label = "Delete",
is_available = can_act_on_entities
)]
pub(crate) fn entity_delete(_: In<OperatorParameters>, mut commands: Commands) -> OperatorResult {
commands.queue(delete_selected);
OperatorResult::Finished
}
#[operator(
id = "entity.duplicate",
label = "Duplicate",
is_available = can_act_on_entities
)]
pub(crate) fn entity_duplicate(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(duplicate_selected);
OperatorResult::Finished
}
#[operator(
id = "entity.copy_components",
label = "Copy Components",
allows_undo = false,
is_available = can_act_on_entities
)]
pub(crate) fn entity_copy_components(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(copy_components);
OperatorResult::Finished
}
#[operator(
id = "entity.paste_components",
label = "Paste Components",
is_available = can_act_on_entities
)]
pub(crate) fn entity_paste_components(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(paste_components);
OperatorResult::Finished
}
#[operator(
id = "entity.toggle_visibility",
label = "Toggle Visibility",
allows_undo = false,
is_available = can_act_on_entities
)]
pub(crate) fn entity_toggle_visibility(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(hide_selected);
OperatorResult::Finished
}
#[operator(
id = "entity.hide_unselected",
label = "Hide Unselected",
allows_undo = false,
is_available = can_act_on_entities
)]
pub(crate) fn entity_hide_unselected(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
if let Err(err) = world.run_system_cached(hide_all_entities) {
warn!("hide_all_entities: {err:?}");
}
});
OperatorResult::Finished
}
#[operator(
id = "entity.unhide_all",
label = "Unhide All",
allows_undo = false,
is_available = can_act_on_entities
)]
pub(crate) fn entity_unhide_all(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
if let Err(err) = world.run_system_cached(unhide_all_entities) {
warn!("unhide_all_entities: {err:?}");
}
});
OperatorResult::Finished
}
#[operator(id = "entity.add.cube", label = "Cube")]
pub(crate) fn entity_add_cube(_: In<OperatorParameters>, mut commands: Commands) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::Cube);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.sphere", label = "Sphere")]
pub(crate) fn entity_add_sphere(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::Sphere);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.point_light", label = "Point Light")]
pub(crate) fn entity_add_point_light(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::PointLight);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.directional_light", label = "Directional Light")]
pub(crate) fn entity_add_directional_light(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::DirectionalLight);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.spot_light", label = "Spot Light")]
pub(crate) fn entity_add_spot_light(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::SpotLight);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.camera", label = "Camera")]
pub(crate) fn entity_add_camera(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::Camera3d);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.empty", label = "Empty")]
pub(crate) fn entity_add_empty(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
create_entity_in_world(world, EntityTemplate::Empty);
});
OperatorResult::Finished
}
#[operator(id = "entity.add.navmesh", label = "Navmesh")]
pub(crate) fn entity_add_navmesh(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
crate::spawn_undoable(world, "Add Navmesh Region", |world| {
let mut system_state: SystemState<(Commands, ResMut<Selection>)> =
SystemState::new(world);
let (mut commands, mut selection) = system_state.get_mut(world);
let entity = crate::navmesh::spawn_navmesh_entity(&mut commands);
selection.select_single(&mut commands, entity);
system_state.apply(world);
crate::scene_io::register_entity_in_ast(world, entity);
entity
});
});
OperatorResult::Finished
}
#[operator(id = "entity.add.terrain", label = "Terrain")]
pub(crate) fn entity_add_terrain(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
crate::spawn_undoable(world, "Add Terrain", |world| {
let mut system_state: SystemState<(Commands, ResMut<Selection>)> =
SystemState::new(world);
let (mut commands, mut selection) = system_state.get_mut(world);
let entity = crate::terrain::spawn_terrain_entity(&mut commands);
selection.select_single(&mut commands, entity);
system_state.apply(world);
crate::scene_io::register_entity_in_ast(world, entity);
entity
});
});
OperatorResult::Finished
}
#[operator(id = "entity.add.prefab", label = "Prefab")]
pub(crate) fn entity_add_prefab(
_: In<OperatorParameters>,
mut commands: Commands,
) -> OperatorResult {
commands.queue(|world: &mut World| {
crate::prefab_picker::open_prefab_picker(world);
});
OperatorResult::Finished
}