Compare commits
No commits in common. "2fa64106c82a5426d2af12ad7c8c89be63065a98" and "3e053a81abec93973c7a3036d6af837610501e19" have entirely different histories.
2fa64106c8
...
3e053a81ab
16
.github/pull_request_template.md
vendored
16
.github/pull_request_template.md
vendored
@ -1,16 +0,0 @@
|
||||
## Summary
|
||||
|
||||
-
|
||||
|
||||
## Validation
|
||||
|
||||
-
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] I updated the root README for user controls, run commands, or troubleshooting changes.
|
||||
- [ ] I updated `docs/editor/` for editor workflow or feature-design changes.
|
||||
- [ ] I added or updated an ADR for architecture, policy, scene format, rendering path, PIE semantics, settings schema, or netcode decisions.
|
||||
- [ ] I indexed new documentation in `docs/README.md`.
|
||||
- [ ] If this is Jackdaw-inspired work, I identified it as design-inspired, API-inspired, or code-derived.
|
||||
- [ ] If this includes copied or closely translated source, I included source URL, upstream commit/release, license compatibility notes, and required file-level notices.
|
||||
@ -97,7 +97,7 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
||||
| `Ctrl+G` | Toggle viewport grid |
|
||||
| `F` | Focus editor camera on selection |
|
||||
| `Ctrl+Shift+1` / `Ctrl+Shift+2` | Save / recall viewport camera bookmark |
|
||||
| RMB + mouse | Editor camera look; cursor hides while held |
|
||||
| RMB + mouse | Editor camera look |
|
||||
| RMB + `W` `A` `S` `D` | Editor camera fly |
|
||||
| RMB + `Q` / `E` | Editor camera down / up |
|
||||
| Mouse wheel | Dolly editor camera |
|
||||
@ -117,17 +117,15 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
||||
| Select Project Sun | Inspect project default lighting; create a scene sun override |
|
||||
| Asset Browser project/file views | Browse `assets/`, search/filter/sort, switch grid/list, expand model subassets with generated thumbnails, inspect staged import/material settings, drag assets/submeshes into the viewport |
|
||||
| Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` |
|
||||
| `Ctrl+P` | Command palette; type to filter, use arrow keys to select, Enter to run (`scene.reset_lighting`, `selection.group`, `selection.focus`, `selection.reset_transform`, play commands) |
|
||||
| `Ctrl+P` | Command palette (`scene.reset_lighting`, `selection.group`, `selection.focus`, play commands) |
|
||||
| `F7` | While paused in Play: advance one sim tick |
|
||||
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
||||
| Hierarchy context | Reparent to other selection / Unparent |
|
||||
| File menu | New, Open, Save, Save As, Import Assets, Export Selection, Save Selection As Prefab, Recent Scenes |
|
||||
| Inspector component card | Collapse with caret, toggle active with status dot, or use triple-dot menu for reset/copy/paste/move/remove actions |
|
||||
| Inspector footer → Add Component | Expands an inline search shelf for registered authoring, rendering, physics, gameplay, and volume components with descriptions, availability hints, and undo |
|
||||
| Inspector footer → Add Component | Pinned search/add control for registered authoring, rendering, physics, and gameplay components with undo |
|
||||
| Edit → Project Settings… | Edit `assets/project.ron` (rendering, physics, input) |
|
||||
|
||||
Viewport shortcut keys require the pointer to be in the viewport and are suspended while typing in egui text fields or actively using camera navigation. `Delete`, `Backspace`, duplicate, and undo/redo also defer to text-field focus.
|
||||
|
||||
### Play Mode
|
||||
|
||||
- Press **F5** (or Play menu) to run the **real game** in-process: same `GamePlugin`, player,
|
||||
|
||||
@ -8,7 +8,7 @@ use crate::state::{EditorMode, PlayPossession};
|
||||
use crate::ui::helpers::{
|
||||
reset_scene_lighting_to_project_defaults, toggle_play_mode, toggle_play_paused,
|
||||
};
|
||||
use crate::ui::selection_ops::{focus_editor_camera_on_selection, reset_selected_transforms};
|
||||
use crate::ui::selection_ops::focus_editor_camera_on_selection;
|
||||
use crate::ui::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM};
|
||||
use crate::ui::UiState;
|
||||
|
||||
@ -127,8 +127,6 @@ pub struct CommandPalette {
|
||||
pub open: bool,
|
||||
pub filter: String,
|
||||
pub pending_run: Option<String>,
|
||||
pub focus_filter: bool,
|
||||
pub selected_index: usize,
|
||||
}
|
||||
|
||||
pub struct ExtensibilityPlugin;
|
||||
@ -162,7 +160,6 @@ fn register_builtin_commands(mut registry: ResMut<EditorCommandRegistry>) {
|
||||
registry.register(Box::new(ResetLightingCommand));
|
||||
registry.register(Box::new(GroupSelectionCommand));
|
||||
registry.register(Box::new(FocusSelectionCommand));
|
||||
registry.register(Box::new(ResetSelectionTransformCommand));
|
||||
registry.register(Box::new(CreatePostProcessVolumeCommand));
|
||||
registry.register(Box::new(FocusActiveVolumesCommand));
|
||||
registry.register(Box::new(SelectVolumesAtCameraCommand));
|
||||
@ -275,18 +272,6 @@ impl EditorCommand for FocusSelectionCommand {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetSelectionTransformCommand;
|
||||
|
||||
impl EditorCommand for ResetSelectionTransformCommand {
|
||||
fn name(&self) -> &str {
|
||||
"selection.reset_transform"
|
||||
}
|
||||
|
||||
fn execute(&self, world: &mut World) {
|
||||
reset_selected_transforms(world);
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatePostProcessVolumeCommand;
|
||||
|
||||
impl EditorCommand for CreatePostProcessVolumeCommand {
|
||||
@ -337,8 +322,6 @@ fn command_palette_ui(
|
||||
|
||||
if ctx.input(|input| input.modifiers.command && input.key_pressed(egui::Key::P)) {
|
||||
palette.open = true;
|
||||
palette.focus_filter = true;
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
if !palette.open {
|
||||
@ -356,71 +339,25 @@ fn command_palette_ui(
|
||||
.color(TEXT_DIM)
|
||||
.small(),
|
||||
);
|
||||
let filter_response = ui.add(
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut palette.filter)
|
||||
.hint_text("Type to filter...")
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
if palette.focus_filter {
|
||||
filter_response.request_focus();
|
||||
palette.focus_filter = false;
|
||||
}
|
||||
if filter_response.changed() {
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
let filter = palette.filter.to_lowercase();
|
||||
let filtered_names: Vec<String> = registry
|
||||
.names()
|
||||
.into_iter()
|
||||
.filter(|name| filter.is_empty() || name.to_lowercase().contains(&filter))
|
||||
.collect();
|
||||
if !filtered_names.is_empty() {
|
||||
palette.selected_index = palette.selected_index.min(filtered_names.len() - 1);
|
||||
} else {
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
|
||||
&& !filtered_names.is_empty()
|
||||
{
|
||||
palette.selected_index = (palette.selected_index + 1) % filtered_names.len();
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
|
||||
&& !filtered_names.is_empty()
|
||||
{
|
||||
palette.selected_index = if palette.selected_index == 0 {
|
||||
filtered_names.len() - 1
|
||||
} else {
|
||||
palette.selected_index - 1
|
||||
};
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
|
||||
if let Some(name) = filtered_names.get(palette.selected_index) {
|
||||
palette.pending_run = Some(name.clone());
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(280.0)
|
||||
.show(ui, |ui| {
|
||||
for (index, name) in filtered_names.iter().enumerate() {
|
||||
let selected = index == palette.selected_index;
|
||||
let response =
|
||||
ui.selectable_label(selected, egui::RichText::new(name).color(TEXT));
|
||||
if selected {
|
||||
response.scroll_to_me(Some(egui::Align::Center));
|
||||
for name in registry.names() {
|
||||
if !palette.filter.is_empty()
|
||||
&& !name.to_lowercase().contains(&palette.filter.to_lowercase())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let response =
|
||||
ui.selectable_label(false, egui::RichText::new(&name).color(TEXT));
|
||||
if response.clicked() {
|
||||
palette.selected_index = index;
|
||||
palette.pending_run = Some(name.clone());
|
||||
palette.pending_run = Some(name);
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ use crate::ui::hierarchy_ops::{
|
||||
apply_sibling_change, apply_sibling_change_new, next_sibling_index,
|
||||
reorder_entities_under_parent, would_create_cycle,
|
||||
};
|
||||
use crate::ui::{egui_captures_keyboard_from_world, UiState};
|
||||
use crate::ui::UiState;
|
||||
|
||||
mod commands;
|
||||
pub use commands::{EditorCommand, EditorEntitySnapshot, SiblingChange};
|
||||
@ -637,42 +637,9 @@ pub fn set_transform_with_history(
|
||||
if transform_nearly_eq(&old, &new) {
|
||||
return;
|
||||
}
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
*transform = new;
|
||||
}
|
||||
push_history(world, EditorCommand::SetTransform { entity, old, new });
|
||||
}
|
||||
|
||||
pub fn set_transform_group_with_history(
|
||||
world: &mut World,
|
||||
changes: impl IntoIterator<Item = (Entity, Transform, Transform)>,
|
||||
) {
|
||||
let mut entities = Vec::new();
|
||||
let mut olds = Vec::new();
|
||||
let mut news = Vec::new();
|
||||
for (entity, old, new) in changes {
|
||||
if transform_nearly_eq(&old, &new) {
|
||||
continue;
|
||||
}
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
*transform = new;
|
||||
entities.push(entity);
|
||||
olds.push(old);
|
||||
news.push(new);
|
||||
}
|
||||
}
|
||||
if !entities.is_empty() {
|
||||
push_history(
|
||||
world,
|
||||
EditorCommand::SetTransformGroup {
|
||||
entities,
|
||||
olds,
|
||||
news,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_actor_kind_with_history(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
@ -1189,10 +1156,6 @@ fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &Edit
|
||||
}
|
||||
|
||||
fn handle_history_hotkeys(world: &mut World) {
|
||||
if egui_captures_keyboard_from_world(world) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (undo, redo, delete, duplicate) = {
|
||||
let keys = world.resource::<ButtonInput<KeyCode>>();
|
||||
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
|
||||
@ -1256,7 +1219,7 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
let start_group =
|
||||
active.map(|(entity, transform)| selected_transforms(world, &selection, entity, transform));
|
||||
|
||||
let (finished_edit, live_edit) = {
|
||||
let finished_edit = {
|
||||
let mut tracker = world.resource_mut::<TransformEditTracker>();
|
||||
if tracker.active.is_none() {
|
||||
if let (Some((entity, transform)), Some(group)) = (active, start_group) {
|
||||
@ -1265,26 +1228,15 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
}
|
||||
}
|
||||
match tracker.active.take() {
|
||||
Some((primary, old, group)) if active.is_none() => (Some((primary, old, group)), None),
|
||||
Some((primary, old, group)) if active.is_none() => Some((primary, old, group)),
|
||||
Some(state) if active.is_some() => {
|
||||
let live = Some(state.clone());
|
||||
tracker.active = Some(state);
|
||||
(None, live)
|
||||
None
|
||||
}
|
||||
_ => (None, None),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((primary, old_primary, group)) = live_edit {
|
||||
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
||||
return;
|
||||
};
|
||||
if group.len() > 1 {
|
||||
let changes = transform_group_changes(old_primary, new_primary, group);
|
||||
apply_transform_group(world, &changes);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((primary, old_primary, group)) = finished_edit {
|
||||
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
||||
return;
|
||||
@ -1293,36 +1245,34 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
set_transform_with_history(world, primary, old_primary, new_primary);
|
||||
return;
|
||||
}
|
||||
let changes = transform_group_changes(old_primary, new_primary, group);
|
||||
set_transform_group_with_history(world, changes);
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_group_changes(
|
||||
old_primary: Transform,
|
||||
new_primary: Transform,
|
||||
group: Vec<(Entity, Transform)>,
|
||||
) -> Vec<(Entity, Transform, Transform)> {
|
||||
let translation_delta = new_primary.translation - old_primary.translation;
|
||||
let rotation_delta = new_primary.rotation * old_primary.rotation.inverse();
|
||||
let scale_ratio = new_primary.scale / old_primary.scale;
|
||||
group
|
||||
.into_iter()
|
||||
.map(|(entity, old)| {
|
||||
let mut entities = Vec::new();
|
||||
let mut olds = Vec::new();
|
||||
let mut news = Vec::new();
|
||||
for (entity, old) in group {
|
||||
let new = Transform {
|
||||
translation: old.translation + translation_delta,
|
||||
rotation: rotation_delta * old.rotation,
|
||||
scale: old.scale * scale_ratio,
|
||||
};
|
||||
(entity, old, new)
|
||||
})
|
||||
.collect()
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
*transform = new;
|
||||
}
|
||||
|
||||
fn apply_transform_group(world: &mut World, changes: &[(Entity, Transform, Transform)]) {
|
||||
for (entity, _, new) in changes {
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(*entity) {
|
||||
*transform = *new;
|
||||
entities.push(entity);
|
||||
olds.push(old);
|
||||
news.push(new);
|
||||
}
|
||||
if !entities.is_empty() {
|
||||
push_history(
|
||||
world,
|
||||
EditorCommand::SetTransformGroup {
|
||||
entities,
|
||||
olds,
|
||||
news,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,12 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
let footer_reserve = 52.0;
|
||||
let body_height = (ui.available_height() - footer_reserve).max(1.0);
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::vec2(ui.available_width(), body_height),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
@ -103,9 +109,10 @@ pub fn draw_actor_inspector(world: &mut World, ui: &mut egui::Ui, entity: Entity
|
||||
world.resource_scope(|world, registry: Mut<ActorInspectorSectionRegistry>| {
|
||||
registry.draw_sections(world, ui, entity);
|
||||
});
|
||||
|
||||
inspector::add_component_footer(world, ui, entity);
|
||||
});
|
||||
},
|
||||
);
|
||||
inspector::add_component_footer(world, ui, entity);
|
||||
}
|
||||
|
||||
fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
||||
|
||||
@ -5,11 +5,9 @@ use egui_phosphor_icons::icons;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EditorComponentCategory {
|
||||
Authoring,
|
||||
Rendering,
|
||||
Physics,
|
||||
Gameplay,
|
||||
Volumes,
|
||||
Editor,
|
||||
}
|
||||
|
||||
@ -21,13 +19,8 @@ pub struct EditorComponentDescriptor {
|
||||
pub addable: bool,
|
||||
pub removable: bool,
|
||||
pub reorderable: bool,
|
||||
pub hidden: bool,
|
||||
pub icon: &'static str,
|
||||
pub description: &'static str,
|
||||
pub search_terms: &'static [&'static str],
|
||||
pub recommended: &'static [&'static str],
|
||||
pub conflicts_with: &'static [&'static str],
|
||||
pub hydration_effect: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
@ -46,14 +39,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::CUBE.as_str(),
|
||||
description: "Renders one or more normalized imported mesh slots.",
|
||||
search_terms: &["mesh", "renderer", "rendering", "model", "static"],
|
||||
recommended: &[],
|
||||
conflicts_with: &["shared::components::Primitive"],
|
||||
hydration_effect:
|
||||
"Hydrates into generated mesh children and material bindings.",
|
||||
search_terms: &["mesh", "renderer", "rendering", "model"],
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::MaterialDesc",
|
||||
@ -62,13 +49,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::PALETTE.as_str(),
|
||||
description: "Defines an authored material directly on this actor.",
|
||||
search_terms: &["material", "shader", "texture"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into Bevy material assets for renderable actors.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::RigidBodyDesc",
|
||||
@ -77,13 +59,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SPHERE.as_str(),
|
||||
description: "Controls authored rigid-body behavior for physics hydration.",
|
||||
search_terms: &["body", "physics", "rigidbody"],
|
||||
recommended: &["shared::components::ColliderDesc"],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into runtime Avian rigid-body state.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::ColliderDesc",
|
||||
@ -92,13 +69,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SELECTION.as_str(),
|
||||
description: "Defines authored collision shape data.",
|
||||
search_terms: &["collision", "trigger", "physics"],
|
||||
recommended: &["shared::components::RigidBodyDesc"],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into runtime Avian collider state.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::LightDesc",
|
||||
@ -107,29 +79,18 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::LIGHTBULB.as_str(),
|
||||
description: "Adds authored point, spot, directional, or area lighting.",
|
||||
search_terms: &["light", "lamp", "illumination", "sun"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into runtime Bevy light components.",
|
||||
search_terms: &["light", "lamp", "illumination"],
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::Primitive",
|
||||
display_name: "Primitive",
|
||||
category: EditorComponentCategory::Authoring,
|
||||
category: EditorComponentCategory::Rendering,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::CUBE.as_str(),
|
||||
description: "Creates an authored built-in primitive shape.",
|
||||
search_terms: &["box", "sphere", "ramp", "primitive"],
|
||||
recommended: &[],
|
||||
conflicts_with: &["shared::components::StaticMeshRenderer"],
|
||||
hydration_effect:
|
||||
"Hydrates into generated primitive render and collision data.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::PlayerSpawn",
|
||||
@ -138,14 +99,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::PERSON_SIMPLE_RUN.as_str(),
|
||||
description: "Marks where Play mode should spawn the player.",
|
||||
search_terms: &["player", "spawn", "start"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect:
|
||||
"Used by PIE/session startup; no saved runtime player is created.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::WeaponSpawn",
|
||||
@ -154,28 +109,18 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::CROSSHAIR.as_str(),
|
||||
description: "Marks a gameplay weapon spawn point.",
|
||||
search_terms: &["weapon", "spawn"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::TriggerVolume",
|
||||
display_name: "Trigger Volume",
|
||||
category: EditorComponentCategory::Volumes,
|
||||
category: EditorComponentCategory::Gameplay,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::SELECTION.as_str(),
|
||||
description: "Adds an authored gameplay trigger volume.",
|
||||
search_terms: &["trigger", "volume", "event"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into gameplay trigger queries and visualizers.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::TeamSpawn",
|
||||
@ -184,13 +129,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::FLAG.as_str(),
|
||||
description: "Marks a gameplay team spawn point.",
|
||||
search_terms: &["team", "spawn"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::ObjectiveMarker",
|
||||
@ -199,29 +139,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::FLAG.as_str(),
|
||||
description: "Marks an authored gameplay objective location.",
|
||||
search_terms: &["objective", "marker", "gameplay"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect: "Hydrates into gameplay objective metadata.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::PostProcessVolumeDesc",
|
||||
display_name: "Post-Process Volume",
|
||||
category: EditorComponentCategory::Volumes,
|
||||
addable: true,
|
||||
removable: true,
|
||||
reorderable: true,
|
||||
hidden: false,
|
||||
icon: icons::APERTURE.as_str(),
|
||||
description: "Adds a local rendering profile override volume.",
|
||||
search_terms: &["post", "process", "volume", "rendering", "fx"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect:
|
||||
"Affects active camera rendering when the camera is inside the volume.",
|
||||
},
|
||||
EditorComponentDescriptor {
|
||||
type_name: "shared::components::InspectorOrder",
|
||||
@ -230,14 +149,8 @@ impl Default for EditorComponentRegistry {
|
||||
addable: false,
|
||||
removable: false,
|
||||
reorderable: false,
|
||||
hidden: true,
|
||||
icon: icons::LIST_BULLETS.as_str(),
|
||||
description: "Stores editor-only component ordering and active-state metadata.",
|
||||
search_terms: &["editor", "order"],
|
||||
recommended: &[],
|
||||
conflicts_with: &[],
|
||||
hydration_effect:
|
||||
"Controls editor presentation and authoring component active state.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@ -34,9 +34,7 @@ use crate::ui::theme::{
|
||||
};
|
||||
use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
|
||||
|
||||
use super::component_registry::{
|
||||
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
|
||||
};
|
||||
use super::component_registry::{EditorComponentCategory, EditorComponentRegistry};
|
||||
use super::dock_tabs::open_and_focus_tab;
|
||||
use super::helpers::create_scene_sun_override_from_project_settings;
|
||||
use super::{EditorTab, UiState};
|
||||
@ -103,22 +101,6 @@ pub(crate) struct InspectorClipboard {
|
||||
component: Option<CopiedComponent>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone)]
|
||||
pub(crate) struct InspectorPanelState {
|
||||
add_component_search: String,
|
||||
add_component_open: bool,
|
||||
add_component_focus_search: bool,
|
||||
add_component_scroll_selected: bool,
|
||||
add_component_selected_index: usize,
|
||||
add_component_target: Option<Entity>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AddComponentShelfDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CopiedComponent {
|
||||
Primitive(Primitive),
|
||||
@ -682,420 +664,90 @@ fn draw_authoring_component_by_type(
|
||||
}
|
||||
|
||||
pub(crate) fn add_component_footer(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||
let descriptors = world
|
||||
.resource::<EditorComponentRegistry>()
|
||||
.descriptors
|
||||
.clone();
|
||||
ui.add_space(4.0);
|
||||
let button_response = egui::Frame::new()
|
||||
egui::Frame::new()
|
||||
.fill(WIDGET_BG)
|
||||
.stroke(egui::Stroke::new(1.0, BORDER))
|
||||
.corner_radius(egui::CornerRadius::same(4))
|
||||
.inner_margin(egui::Margin::symmetric(8, 6))
|
||||
.show(ui, |ui| {
|
||||
let open_for_entity = add_component_picker_open_for(world, entity);
|
||||
let label = if open_for_entity {
|
||||
"Close Add Component"
|
||||
} else {
|
||||
"+ Add Component"
|
||||
};
|
||||
if ui.button(label).clicked() {
|
||||
if let Some(mut state) = world.get_resource_mut::<InspectorPanelState>() {
|
||||
if open_for_entity {
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
} else {
|
||||
state.add_component_open = true;
|
||||
state.add_component_focus_search = true;
|
||||
state.add_component_scroll_selected = true;
|
||||
state.add_component_selected_index = 0;
|
||||
state.add_component_target = Some(entity);
|
||||
state.add_component_search.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if add_component_picker_open_for(world, entity) {
|
||||
add_component_picker_shelf(world, ui, entity, button_response.response.rect);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_component_picker_open_for(world: &World, entity: Entity) -> bool {
|
||||
world
|
||||
.get_resource::<InspectorPanelState>()
|
||||
.is_some_and(|state| state.add_component_open && state.add_component_target == Some(entity))
|
||||
}
|
||||
|
||||
fn shelf_list_max_height(visible_space: f32) -> f32 {
|
||||
(visible_space - 108.0).clamp(56.0, 320.0)
|
||||
}
|
||||
|
||||
fn add_component_picker_shelf(
|
||||
world: &mut World,
|
||||
ui: &mut egui::Ui,
|
||||
target: Entity,
|
||||
anchor: egui::Rect,
|
||||
) {
|
||||
if !world.get_entity(target).is_ok() {
|
||||
if let Some(mut state) = world.get_resource_mut::<InspectorPanelState>() {
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let descriptors = world
|
||||
.resource::<EditorComponentRegistry>()
|
||||
.descriptors
|
||||
.clone();
|
||||
|
||||
let visible = ui.clip_rect().intersect(ui.ctx().available_rect());
|
||||
let space_above = (anchor.min.y - visible.top()).max(0.0);
|
||||
let space_below = (visible.bottom() - anchor.max.y).max(0.0);
|
||||
let direction = if space_below >= space_above {
|
||||
AddComponentShelfDirection::Down
|
||||
} else {
|
||||
AddComponentShelfDirection::Up
|
||||
};
|
||||
let visible_space = match direction {
|
||||
AddComponentShelfDirection::Up => space_above,
|
||||
AddComponentShelfDirection::Down => space_below,
|
||||
};
|
||||
let list_max_height = shelf_list_max_height(visible_space);
|
||||
let shelf_height = (list_max_height + 108.0).min((visible_space - 4.0).max(96.0));
|
||||
let x = anchor
|
||||
.min
|
||||
.x
|
||||
.clamp(visible.left(), visible.right() - anchor.width());
|
||||
let y = match direction {
|
||||
AddComponentShelfDirection::Up => anchor.min.y - shelf_height - 4.0,
|
||||
AddComponentShelfDirection::Down => anchor.max.y + 4.0,
|
||||
}
|
||||
.clamp(
|
||||
visible.top(),
|
||||
(visible.bottom() - shelf_height).max(visible.top()),
|
||||
);
|
||||
let shelf_width = anchor.width().max(260.0).min(visible.width());
|
||||
|
||||
egui::Area::new(egui::Id::new(("add_component_shelf", target)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(egui::pos2(x, y))
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.set_width(shelf_width);
|
||||
add_component_picker_shelf_contents(world, ui, target, &descriptors, list_max_height);
|
||||
});
|
||||
}
|
||||
|
||||
fn add_component_picker_shelf_contents(
|
||||
world: &mut World,
|
||||
ui: &mut egui::Ui,
|
||||
target: Entity,
|
||||
descriptors: &[EditorComponentDescriptor],
|
||||
list_max_height: f32,
|
||||
) {
|
||||
egui::Frame::new()
|
||||
.fill(PANEL_BG_DARK)
|
||||
.stroke(egui::Stroke::new(1.0, BORDER))
|
||||
.corner_radius(egui::CornerRadius::same(4))
|
||||
.inner_margin(egui::Margin::symmetric(8, 8))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(panel_heading("Add Component"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icon_button_small(ui, icons::X, "Close Add Component").clicked() {
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
|
||||
let mut search_input = world
|
||||
.resource::<InspectorPanelState>()
|
||||
.add_component_search
|
||||
.clone();
|
||||
let search_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut search_input)
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let mut search = String::new();
|
||||
let search_width = fit_width(ui, MIN_INLINE_CONTROL_WIDTH, TEXT_FIELD_MAX_WIDTH);
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut search)
|
||||
.hint_text("Search components...")
|
||||
.desired_width(f32::INFINITY),
|
||||
.desired_width(search_width),
|
||||
);
|
||||
if search_response.changed() {
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_search = search_input.clone();
|
||||
state.add_component_scroll_selected = true;
|
||||
state.add_component_selected_index = 0;
|
||||
}
|
||||
if world
|
||||
.resource::<InspectorPanelState>()
|
||||
.add_component_focus_search
|
||||
{
|
||||
search_response.request_focus();
|
||||
world
|
||||
.resource_mut::<InspectorPanelState>()
|
||||
.add_component_focus_search = false;
|
||||
}
|
||||
|
||||
let search = search_input.to_lowercase();
|
||||
let filtered = filtered_component_descriptors(&descriptors, &search);
|
||||
{
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
if !filtered.is_empty() {
|
||||
state.add_component_selected_index =
|
||||
state.add_component_selected_index.min(filtered.len() - 1);
|
||||
} else {
|
||||
state.add_component_selected_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
|
||||
&& !filtered.is_empty()
|
||||
{
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_selected_index =
|
||||
(state.add_component_selected_index + 1) % filtered.len();
|
||||
state.add_component_scroll_selected = true;
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
|
||||
&& !filtered.is_empty()
|
||||
{
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_selected_index = if state.add_component_selected_index == 0 {
|
||||
filtered.len() - 1
|
||||
} else {
|
||||
state.add_component_selected_index - 1
|
||||
};
|
||||
state.add_component_scroll_selected = true;
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
|
||||
let selected_index = world
|
||||
.resource::<InspectorPanelState>()
|
||||
.add_component_selected_index;
|
||||
if let Some(descriptor) = filtered.get(selected_index) {
|
||||
let add_state = component_add_state(world, target, descriptor, &descriptors);
|
||||
if add_state.addable {
|
||||
insert_registered_component(world, target, descriptor.type_name);
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
if filtered.is_empty() {
|
||||
ui.label(egui::RichText::new("No matching components").color(TEXT_DIM));
|
||||
return;
|
||||
}
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(list_max_height)
|
||||
.show(ui, |ui| {
|
||||
let (selected_index, scroll_selected) = {
|
||||
let state = world.resource::<InspectorPanelState>();
|
||||
(
|
||||
state.add_component_selected_index,
|
||||
state.add_component_scroll_selected,
|
||||
)
|
||||
};
|
||||
let mut last_category = None;
|
||||
for (index, descriptor) in filtered.iter().enumerate() {
|
||||
if last_category != Some(descriptor.category) {
|
||||
if last_category.is_some() {
|
||||
ui.separator();
|
||||
}
|
||||
ui.label(panel_heading(component_category_label(descriptor.category)));
|
||||
last_category = Some(descriptor.category);
|
||||
}
|
||||
|
||||
let add_state =
|
||||
component_add_state(world, target, descriptor, &descriptors);
|
||||
let selected = index == selected_index;
|
||||
let row = ui
|
||||
.horizontal(|ui| {
|
||||
ui.label(phosphor_icon_text(descriptor.icon, 14.0).color(TEXT_DIM));
|
||||
ui.add_enabled(
|
||||
add_state.addable,
|
||||
egui::Button::selectable(selected, descriptor.display_name),
|
||||
)
|
||||
})
|
||||
.inner
|
||||
.on_hover_text(component_hover_text(descriptor, &add_state));
|
||||
if selected && scroll_selected {
|
||||
row.scroll_to_me(Some(egui::Align::Center));
|
||||
}
|
||||
if row.clicked() {
|
||||
world
|
||||
.resource_mut::<InspectorPanelState>()
|
||||
.add_component_selected_index = index;
|
||||
if add_state.addable {
|
||||
insert_registered_component(world, target, descriptor.type_name);
|
||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
||||
state.add_component_open = false;
|
||||
state.add_component_target = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
world
|
||||
.resource_mut::<InspectorPanelState>()
|
||||
.add_component_scroll_selected = false;
|
||||
});
|
||||
}
|
||||
|
||||
fn filtered_component_descriptors<'a>(
|
||||
descriptors: &'a [EditorComponentDescriptor],
|
||||
search: &str,
|
||||
) -> Vec<&'a EditorComponentDescriptor> {
|
||||
[
|
||||
EditorComponentCategory::Authoring,
|
||||
ui.menu_button("Add Component", |ui| {
|
||||
for category in [
|
||||
EditorComponentCategory::Rendering,
|
||||
EditorComponentCategory::Physics,
|
||||
EditorComponentCategory::Gameplay,
|
||||
EditorComponentCategory::Volumes,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|category| {
|
||||
descriptors.iter().filter(move |descriptor| {
|
||||
descriptor.addable
|
||||
&& !descriptor.hidden
|
||||
&& descriptor.category == category
|
||||
&& descriptor_matches_search(descriptor, search)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
] {
|
||||
let mut wrote_category = false;
|
||||
for descriptor in descriptors.iter().filter(|descriptor| {
|
||||
descriptor.addable && descriptor.category == category
|
||||
}) {
|
||||
if !component_addable(world, entity, descriptor.type_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fn component_category_label(category: EditorComponentCategory) -> &'static str {
|
||||
match category {
|
||||
EditorComponentCategory::Authoring => "Authoring",
|
||||
if !wrote_category {
|
||||
ui.label(panel_heading(match category {
|
||||
EditorComponentCategory::Rendering => "Rendering",
|
||||
EditorComponentCategory::Physics => "Physics",
|
||||
EditorComponentCategory::Gameplay => "Gameplay",
|
||||
EditorComponentCategory::Volumes => "Volumes",
|
||||
EditorComponentCategory::Editor => "Editor",
|
||||
}));
|
||||
wrote_category = true;
|
||||
}
|
||||
}
|
||||
|
||||
struct ComponentAddState {
|
||||
addable: bool,
|
||||
reason: Option<String>,
|
||||
recommended: Vec<&'static str>,
|
||||
conflicts: Vec<&'static str>,
|
||||
}
|
||||
|
||||
fn descriptor_matches_search(descriptor: &EditorComponentDescriptor, search: &str) -> bool {
|
||||
search.trim().is_empty()
|
||||
|| descriptor.display_name.to_lowercase().contains(search)
|
||||
|| descriptor.type_name.to_lowercase().contains(search)
|
||||
|| descriptor.description.to_lowercase().contains(search)
|
||||
|| descriptor
|
||||
.search_terms
|
||||
.iter()
|
||||
.any(|term| term.to_lowercase().contains(search))
|
||||
}
|
||||
|
||||
fn component_add_state(
|
||||
world: &World,
|
||||
entity: Entity,
|
||||
descriptor: &EditorComponentDescriptor,
|
||||
descriptors: &[EditorComponentDescriptor],
|
||||
) -> ComponentAddState {
|
||||
let duplicate = component_present(world, entity, descriptor.type_name);
|
||||
let conflicts = descriptor
|
||||
.conflicts_with
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|type_name| component_present(world, entity, type_name))
|
||||
.collect::<Vec<_>>();
|
||||
let recommended = descriptor
|
||||
.recommended
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|type_name| !component_present(world, entity, type_name))
|
||||
.collect::<Vec<_>>();
|
||||
let reason = if duplicate {
|
||||
Some("Already present on this actor.".to_string())
|
||||
} else if !conflicts.is_empty() {
|
||||
Some(format!(
|
||||
"Conflicts with {}.",
|
||||
conflicts
|
||||
.iter()
|
||||
.map(|type_name| component_display_name(descriptors, type_name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
let mut clicked = false;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(phosphor_icon_text(descriptor.icon, 14.0).color(TEXT_DIM));
|
||||
clicked = ui
|
||||
.button(descriptor.display_name)
|
||||
.on_hover_text(format!(
|
||||
"{} | removable={} reorderable={} | {}",
|
||||
descriptor.type_name,
|
||||
descriptor.removable,
|
||||
descriptor.reorderable,
|
||||
descriptor.search_terms.join(", ")
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ComponentAddState {
|
||||
addable: reason.is_none(),
|
||||
reason,
|
||||
recommended,
|
||||
conflicts,
|
||||
.clicked();
|
||||
});
|
||||
if clicked {
|
||||
insert_registered_component(world, entity, descriptor.type_name);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if wrote_category {
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn component_hover_text(
|
||||
descriptor: &EditorComponentDescriptor,
|
||||
state: &ComponentAddState,
|
||||
) -> String {
|
||||
let mut lines = vec![
|
||||
descriptor.description.to_string(),
|
||||
format!("Type: {}", descriptor.type_name),
|
||||
format!("Hydration: {}", descriptor.hydration_effect),
|
||||
format!(
|
||||
"Inspector: removable={} reorderable={}",
|
||||
descriptor.removable, descriptor.reorderable
|
||||
),
|
||||
];
|
||||
if let Some(reason) = &state.reason {
|
||||
lines.push(format!("Unavailable: {reason}"));
|
||||
}
|
||||
if !state.recommended.is_empty() {
|
||||
lines.push(format!(
|
||||
"Recommended with: {}",
|
||||
state.recommended.join(", ")
|
||||
));
|
||||
}
|
||||
if !state.conflicts.is_empty() {
|
||||
lines.push(format!("Conflicts: {}", state.conflicts.join(", ")));
|
||||
}
|
||||
lines.push(format!("Search: {}", descriptor.search_terms.join(", ")));
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn component_display_name(
|
||||
descriptors: &[EditorComponentDescriptor],
|
||||
type_name: &str,
|
||||
) -> &'static str {
|
||||
descriptors
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.type_name == type_name)
|
||||
.map(|descriptor| descriptor.display_name)
|
||||
.unwrap_or("component")
|
||||
}
|
||||
|
||||
fn component_present(world: &World, entity: Entity, type_name: &str) -> bool {
|
||||
fn component_addable(world: &World, entity: Entity, type_name: &str) -> bool {
|
||||
match type_name {
|
||||
"shared::components::Primitive" => world.get::<Primitive>(entity).is_some(),
|
||||
"shared::components::Primitive" => world.get::<Primitive>(entity).is_none(),
|
||||
"shared::components::StaticMeshRenderer" => {
|
||||
world.get::<StaticMeshRenderer>(entity).is_some()
|
||||
}
|
||||
"shared::components::MaterialDesc" => world.get::<MaterialDesc>(entity).is_some(),
|
||||
"shared::components::LightDesc" => world.get::<LightDesc>(entity).is_some(),
|
||||
"shared::components::RigidBodyDesc" => world.get::<RigidBodyDesc>(entity).is_some(),
|
||||
"shared::components::ColliderDesc" => world.get::<ColliderDesc>(entity).is_some(),
|
||||
"shared::components::PlayerSpawn" => world.get::<PlayerSpawn>(entity).is_some(),
|
||||
"shared::components::WeaponSpawn" => world.get::<WeaponSpawn>(entity).is_some(),
|
||||
"shared::components::TriggerVolume" => world.get::<TriggerVolume>(entity).is_some(),
|
||||
"shared::components::TeamSpawn" => world.get::<TeamSpawn>(entity).is_some(),
|
||||
"shared::components::ObjectiveMarker" => world.get::<ObjectiveMarker>(entity).is_some(),
|
||||
"shared::components::PostProcessVolumeDesc" => {
|
||||
world.get::<PostProcessVolumeDesc>(entity).is_some()
|
||||
world.get::<StaticMeshRenderer>(entity).is_none()
|
||||
}
|
||||
"shared::components::MaterialDesc" => world.get::<MaterialDesc>(entity).is_none(),
|
||||
"shared::components::LightDesc" => world.get::<LightDesc>(entity).is_none(),
|
||||
"shared::components::RigidBodyDesc" => world.get::<RigidBodyDesc>(entity).is_none(),
|
||||
"shared::components::ColliderDesc" => world.get::<ColliderDesc>(entity).is_none(),
|
||||
"shared::components::PlayerSpawn" => world.get::<PlayerSpawn>(entity).is_none(),
|
||||
"shared::components::WeaponSpawn" => world.get::<WeaponSpawn>(entity).is_none(),
|
||||
"shared::components::TriggerVolume" => world.get::<TriggerVolume>(entity).is_none(),
|
||||
"shared::components::TeamSpawn" => world.get::<TeamSpawn>(entity).is_none(),
|
||||
"shared::components::ObjectiveMarker" => world.get::<ObjectiveMarker>(entity).is_none(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -1139,11 +791,6 @@ fn insert_registered_component(world: &mut World, entity: Entity, type_name: &st
|
||||
objective_id: "objective".into(),
|
||||
});
|
||||
}),
|
||||
"shared::components::PostProcessVolumeDesc" => {
|
||||
insert_component(world, entity, |world, e| {
|
||||
world.entity_mut(e).insert(PostProcessVolumeDesc::default());
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,31 +65,6 @@ pub struct UiState {
|
||||
last_mode_tab: Option<EditorMode>,
|
||||
}
|
||||
|
||||
pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool {
|
||||
ctx.wants_keyboard_input()
|
||||
}
|
||||
|
||||
pub fn egui_captures_keyboard_from_world(world: &mut World) -> bool {
|
||||
let Ok(egui_context) = world
|
||||
.query_filtered::<&mut EguiContext, With<PrimaryEguiContext>>()
|
||||
.single(world)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let mut egui_context = egui_context.clone();
|
||||
egui_captures_keyboard(egui_context.get_mut())
|
||||
}
|
||||
|
||||
pub fn viewport_keyboard_shortcuts_active(
|
||||
ui_state: &UiState,
|
||||
ctx: &egui::Context,
|
||||
buttons: &ButtonInput<MouseButton>,
|
||||
) -> bool {
|
||||
ui_state.pointer_in_viewport
|
||||
&& !egui_captures_keyboard(ctx)
|
||||
&& !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
pub fn default_layout() -> Self {
|
||||
Self::from_prefs(&UserPreferences::default())
|
||||
@ -138,7 +113,6 @@ impl UiState {
|
||||
);
|
||||
|
||||
editor_toolbar_panel(world, ctx);
|
||||
status_bar_ui(world, ctx, &self.selected_entities, mode);
|
||||
|
||||
self.pointer_in_viewport = false;
|
||||
self.viewport_pointer_pos = None;
|
||||
@ -180,6 +154,8 @@ impl UiState {
|
||||
);
|
||||
tick_layout_save(world, dt);
|
||||
|
||||
status_bar_ui(world, ctx, &self.selected_entities, mode);
|
||||
|
||||
let mut diagnostics_open = world.resource::<DiagnosticsPanel>().open;
|
||||
diagnostics_window(world, ctx, &mut diagnostics_open);
|
||||
world.resource_mut::<DiagnosticsPanel>().open = diagnostics_open;
|
||||
@ -250,7 +226,6 @@ impl Plugin for EditorUiPlugin {
|
||||
.init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
||||
.init_resource::<asset_browser::AssetBrowserUiState>()
|
||||
.init_resource::<inspector::InspectorClipboard>()
|
||||
.init_resource::<inspector::InspectorPanelState>()
|
||||
.init_resource::<ViewportUiState>()
|
||||
.init_resource::<LayoutSaveTimer>()
|
||||
.add_systems(
|
||||
|
||||
@ -5,11 +5,8 @@ use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
||||
use shared::LevelObject;
|
||||
|
||||
use crate::camera::EditorCamera;
|
||||
use crate::history::{
|
||||
delete_entities_with_history, duplicate_entities_with_history, set_transform_group_with_history,
|
||||
};
|
||||
use crate::history::{delete_entities_with_history, duplicate_entities_with_history};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::UiState;
|
||||
|
||||
pub fn focus_editor_camera_on_selection(world: &mut World) {
|
||||
let Some(entity) = world.resource::<SelectedEntity>().0 else {
|
||||
@ -61,19 +58,3 @@ pub fn duplicate_selection(world: &mut World, selected: &SelectedEntities) {
|
||||
let entities = selected_level_entities(world, selected);
|
||||
duplicate_entities_with_history(world, &entities);
|
||||
}
|
||||
|
||||
pub fn reset_selected_transforms(world: &mut World) {
|
||||
let changes: Vec<_> = world
|
||||
.resource::<UiState>()
|
||||
.selected_entities
|
||||
.iter()
|
||||
.filter(|entity| is_level_object(world, *entity))
|
||||
.filter_map(|entity| {
|
||||
world
|
||||
.get::<Transform>(entity)
|
||||
.copied()
|
||||
.map(|old| (entity, old, Transform::default()))
|
||||
})
|
||||
.collect();
|
||||
set_transform_group_with_history(world, changes);
|
||||
}
|
||||
|
||||
@ -58,9 +58,8 @@ pub fn viewport_tab_ui(
|
||||
.filter(|pos| rect.contains(*pos));
|
||||
*pointer_in_viewport = viewport_pointer_pos.is_some();
|
||||
|
||||
let primary_clicked = ui.input(|input| {
|
||||
input.pointer.button_clicked(egui::PointerButton::Primary) && !input.modifiers.alt
|
||||
});
|
||||
let primary_clicked =
|
||||
ui.input(|input| input.pointer.button_clicked(egui::PointerButton::Primary));
|
||||
if primary_clicked && !clean_game_view {
|
||||
if let Some(pos) = response
|
||||
.interact_pointer_pos()
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
use bevy::camera::visibility::RenderLayers;
|
||||
use bevy::input::mouse::{AccumulatedMouseMotion, MouseWheel};
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::{CursorOptions, PrimaryWindow};
|
||||
use bevy_egui::{EguiContexts, EguiGlobalSettings, PrimaryEguiContext};
|
||||
use transform_gizmo_bevy::prelude::GizmoCamera;
|
||||
|
||||
use crate::infra::EditorOnly;
|
||||
use crate::state::scene_tools_active;
|
||||
use crate::ui::{egui_captures_keyboard, UiState};
|
||||
use crate::ui::UiState;
|
||||
use settings::ProjectSettings;
|
||||
|
||||
#[derive(Component)]
|
||||
@ -29,24 +28,9 @@ impl Default for EditorCamera {
|
||||
|
||||
pub struct EditorCameraPlugin;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CameraNavigationMode {
|
||||
Look,
|
||||
Pan,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct EditorCameraInputState {
|
||||
mode: Option<CameraNavigationMode>,
|
||||
cursor_hidden_for_look: bool,
|
||||
cursor_visible_before_look: bool,
|
||||
}
|
||||
|
||||
impl Plugin for EditorCameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<EditorCameraInputState>()
|
||||
.add_systems(Startup, spawn_editor_camera)
|
||||
.add_systems(
|
||||
app.add_systems(Startup, spawn_editor_camera).add_systems(
|
||||
Update,
|
||||
(sync_editor_camera_from_settings, update_editor_camera)
|
||||
.chain()
|
||||
@ -120,61 +104,25 @@ fn update_editor_camera(
|
||||
mut scroll: MessageReader<MouseWheel>,
|
||||
mut contexts: EguiContexts,
|
||||
ui_state: Res<UiState>,
|
||||
mut input_state: ResMut<EditorCameraInputState>,
|
||||
mut cameras: Query<(&mut Transform, &EditorCamera)>,
|
||||
mut cursor: Single<&mut CursorOptions, With<PrimaryWindow>>,
|
||||
) -> Result {
|
||||
let ctx = contexts.ctx_mut()?;
|
||||
let in_scene = ui_state.pointer_in_viewport;
|
||||
let navigation_pressed =
|
||||
buttons.pressed(MouseButton::Right) || buttons.pressed(MouseButton::Middle);
|
||||
if !navigation_pressed {
|
||||
input_state.mode = None;
|
||||
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
||||
}
|
||||
if egui_captures_keyboard(ctx) {
|
||||
input_state.mode = None;
|
||||
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
||||
if ctx.wants_keyboard_input() {
|
||||
return Ok(());
|
||||
}
|
||||
if in_scene && buttons.just_pressed(MouseButton::Right) {
|
||||
input_state.mode = Some(CameraNavigationMode::Look);
|
||||
input_state.cursor_visible_before_look = cursor.visible;
|
||||
input_state.cursor_hidden_for_look = true;
|
||||
cursor.visible = false;
|
||||
} else if in_scene && buttons.just_pressed(MouseButton::Middle) {
|
||||
input_state.mode = Some(CameraNavigationMode::Pan);
|
||||
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
||||
}
|
||||
if input_state.mode.is_none() && navigation_pressed {
|
||||
if !in_scene && ctx.wants_pointer_input() {
|
||||
return Ok(());
|
||||
}
|
||||
if !in_scene && input_state.mode.is_none() && ctx.wants_pointer_input() {
|
||||
return Ok(());
|
||||
}
|
||||
if in_scene
|
||||
&& input_state.mode.is_none()
|
||||
&& ctx.wants_pointer_input()
|
||||
&& !buttons.pressed(MouseButton::Right)
|
||||
{
|
||||
if in_scene && ctx.wants_pointer_input() && !buttons.pressed(MouseButton::Right) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dt = time.delta_secs();
|
||||
let raw_scroll_delta: f32 = scroll.read().map(|event| event.y).sum();
|
||||
let scroll_delta = if in_scene && !ctx.wants_pointer_input() {
|
||||
raw_scroll_delta
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let scroll_delta: f32 = scroll.read().map(|event| event.y).sum();
|
||||
|
||||
for (mut transform, camera) in &mut cameras {
|
||||
let looking = matches!(input_state.mode, Some(CameraNavigationMode::Look))
|
||||
&& buttons.pressed(MouseButton::Right);
|
||||
let panning = matches!(input_state.mode, Some(CameraNavigationMode::Pan))
|
||||
&& buttons.pressed(MouseButton::Middle);
|
||||
|
||||
if looking {
|
||||
if buttons.pressed(MouseButton::Right) {
|
||||
let delta = mouse_motion.delta;
|
||||
if delta != Vec2::ZERO {
|
||||
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
|
||||
@ -192,7 +140,6 @@ fn update_editor_camera(
|
||||
let up = Vec3::Y;
|
||||
let mut wish = Vec3::ZERO;
|
||||
|
||||
if looking {
|
||||
if keys.pressed(KeyCode::KeyW) {
|
||||
wish += forward;
|
||||
}
|
||||
@ -211,7 +158,6 @@ fn update_editor_camera(
|
||||
if keys.pressed(KeyCode::KeyQ) {
|
||||
wish -= up;
|
||||
}
|
||||
}
|
||||
|
||||
let sprint = if keys.pressed(KeyCode::ShiftLeft) {
|
||||
3.0
|
||||
@ -221,7 +167,7 @@ fn update_editor_camera(
|
||||
transform.translation += wish.normalize_or_zero() * camera.fly_speed * sprint * dt;
|
||||
transform.translation += forward * scroll_delta * camera.fly_speed * 0.08;
|
||||
|
||||
if panning {
|
||||
if buttons.pressed(MouseButton::Middle) {
|
||||
let delta = mouse_motion.delta;
|
||||
let distance_scale = transform.translation.length().max(1.0).sqrt();
|
||||
transform.translation +=
|
||||
@ -231,13 +177,3 @@ fn update_editor_camera(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_editor_camera_cursor(
|
||||
input_state: &mut EditorCameraInputState,
|
||||
cursor: &mut CursorOptions,
|
||||
) {
|
||||
if input_state.cursor_hidden_for_look {
|
||||
cursor.visible = input_state.cursor_visible_before_look;
|
||||
input_state.cursor_hidden_for_look = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
use bevy::math::Rect;
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::EguiContexts;
|
||||
use transform_gizmo_bevy::prelude::{GizmoMode, GizmoOptions, GizmoOrientation};
|
||||
|
||||
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
|
||||
use crate::ui::UiState;
|
||||
use crate::viewport::ViewportDisplayMode;
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -41,19 +40,12 @@ impl Plugin for EditorGizmoPlugin {
|
||||
|
||||
fn handle_gizmo_hotkeys(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
mut contexts: EguiContexts,
|
||||
ui_state: Res<UiState>,
|
||||
display: Res<ViewportDisplayMode>,
|
||||
mut mode: ResMut<EditorGizmoMode>,
|
||||
mut space: ResMut<EditorGizmoSpace>,
|
||||
) -> Result {
|
||||
) {
|
||||
if display.clean_game_view {
|
||||
return Ok(());
|
||||
}
|
||||
let ctx = contexts.ctx_mut()?;
|
||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::KeyW) {
|
||||
*mode = EditorGizmoMode::Translate;
|
||||
@ -70,7 +62,6 @@ fn handle_gizmo_hotkeys(
|
||||
EditorGizmoSpace::Local => EditorGizmoSpace::World,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Maps the viewport egui panel to gizmo cursor space (required for render-to-texture).
|
||||
|
||||
@ -3,11 +3,10 @@
|
||||
use crate::camera::EditorCamera;
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::state::scene_tools_active;
|
||||
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
|
||||
use crate::viewport::viewport_mode::EditorViewportModePlugin;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::Window;
|
||||
use bevy_egui::{egui, EguiContexts};
|
||||
use bevy_egui::egui;
|
||||
|
||||
/// Viewport display and snapping options (editor-only).
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
@ -156,29 +155,22 @@ pub fn snap_translation(position: Vec3, settings: &ViewportSettings) -> Vec3 {
|
||||
|
||||
fn focus_selection_hotkey(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
mut contexts: EguiContexts,
|
||||
ui_state: Res<UiState>,
|
||||
display: Res<ViewportDisplayMode>,
|
||||
selected: Res<SelectedEntity>,
|
||||
mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
|
||||
targets: Query<&GlobalTransform>,
|
||||
) -> Result {
|
||||
) {
|
||||
if display.clean_game_view {
|
||||
return Ok(());
|
||||
}
|
||||
let ctx = contexts.ctx_mut()?;
|
||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if !keys.just_pressed(KeyCode::KeyF) {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
let Some(entity) = selected.0 else {
|
||||
return Ok(());
|
||||
return;
|
||||
};
|
||||
let Ok(target) = targets.get(entity) else {
|
||||
return Ok(());
|
||||
return;
|
||||
};
|
||||
let center = target.translation();
|
||||
for mut transform in &mut editor_cameras {
|
||||
@ -187,21 +179,13 @@ fn focus_selection_hotkey(
|
||||
transform.translation = center - transform.forward().as_vec3() * distance;
|
||||
transform.look_at(center, Vec3::Y);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn toggle_viewport_hotkeys(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
mut contexts: EguiContexts,
|
||||
ui_state: Res<UiState>,
|
||||
mut settings: ResMut<ViewportSettings>,
|
||||
mut display: ResMut<ViewportDisplayMode>,
|
||||
) -> Result {
|
||||
let ctx = contexts.ctx_mut()?;
|
||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
||||
return Ok(());
|
||||
}
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::KeyG) {
|
||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||
if ctrl {
|
||||
@ -210,26 +194,18 @@ fn toggle_viewport_hotkeys(
|
||||
display.clean_game_view = !display.clean_game_view;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn camera_bookmark_hotkeys(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
mut contexts: EguiContexts,
|
||||
ui_state: Res<UiState>,
|
||||
scene_io: Res<crate::scene_io::SceneIo>,
|
||||
mut bookmarks: ResMut<CameraBookmarks>,
|
||||
mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
|
||||
) -> Result {
|
||||
let ctx = contexts.ctx_mut()?;
|
||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
||||
return Ok(());
|
||||
}
|
||||
) {
|
||||
let shift = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
|
||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||
if !shift || !ctrl {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
let scene_key = scene_io
|
||||
@ -240,20 +216,19 @@ fn camera_bookmark_hotkeys(
|
||||
|
||||
if keys.just_pressed(KeyCode::Digit1) {
|
||||
let Ok(transform) = editor_cameras.single() else {
|
||||
return Ok(());
|
||||
return;
|
||||
};
|
||||
save_camera_bookmark(&mut bookmarks, &scene_key, *transform);
|
||||
}
|
||||
|
||||
if keys.just_pressed(KeyCode::Digit2) {
|
||||
let Some(saved) = recall_camera_bookmark(&bookmarks, &scene_key) else {
|
||||
return Ok(());
|
||||
return;
|
||||
};
|
||||
for mut transform in &mut editor_cameras {
|
||||
*transform = saved;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_viewport_grid(
|
||||
|
||||
@ -92,26 +92,22 @@ fn handle_pick_events(
|
||||
proxies: Query<&EditorVisualizerProxy>,
|
||||
icon_proxies: Query<&ActorIconProxy>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
display: Res<ViewportDisplayMode>,
|
||||
gizmo_targets: Query<&GizmoTarget>,
|
||||
hierarchy: Option<Res<HierarchyPanelState>>,
|
||||
) -> Result {
|
||||
if ui_state.pointer_in_viewport
|
||||
&& !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
|
||||
&& keys.just_pressed(KeyCode::Escape)
|
||||
{
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::Escape) {
|
||||
ui_state.selected_entities.clear();
|
||||
selected.0 = None;
|
||||
pick_stack.entities.clear();
|
||||
pick_stack.index = 0;
|
||||
pick_stack.anchor = None;
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
if display.clean_game_view {
|
||||
viewport_click.0 = None;
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
if gizmo_targets
|
||||
@ -119,7 +115,7 @@ fn handle_pick_events(
|
||||
.any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
|
||||
{
|
||||
viewport_click.0 = None;
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
let additive = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]);
|
||||
@ -138,11 +134,11 @@ fn handle_pick_events(
|
||||
pointer_pos,
|
||||
additive,
|
||||
);
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
if !ui_state.pointer_in_viewport {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
for event in click_events.read() {
|
||||
@ -191,7 +187,6 @@ fn handle_pick_events(
|
||||
additive,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cycle_overlapping_viewport_pick(
|
||||
@ -199,29 +194,25 @@ fn cycle_overlapping_viewport_pick(
|
||||
mut selected: ResMut<SelectedEntity>,
|
||||
mut pick_stack: ResMut<ViewportPickStack>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
buttons: Res<ButtonInput<MouseButton>>,
|
||||
display: Res<ViewportDisplayMode>,
|
||||
) -> Result {
|
||||
) {
|
||||
if display.clean_game_view {
|
||||
return Ok(());
|
||||
}
|
||||
if !ui_state.pointer_in_viewport
|
||||
|| buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
|
||||
{
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if !keys.just_pressed(KeyCode::Tab) || pick_stack.entities.len() < 2 {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]) {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
if !ui_state.pointer_in_viewport {
|
||||
return;
|
||||
}
|
||||
|
||||
pick_stack.index = (pick_stack.index + 1) % pick_stack.entities.len();
|
||||
let entity = pick_stack.entities[pick_stack.index];
|
||||
ui_state.selected_entities.select_replace(entity);
|
||||
selected.0 = Some(entity);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_viewport_pick(
|
||||
|
||||
@ -34,7 +34,6 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
||||
| [0017](adr/0017-normalized-static-mesh-assets.md) | Normalized static mesh assets and renderer placement |
|
||||
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
||||
| [0019](adr/0019-local-bevy-render-timeout-patch.md) | Local `bevy_render` patch for transient Linux swapchain timeouts |
|
||||
| [0020](adr/0020-jackdaw-inspired-editor-roadmap.md) | Jackdaw-inspired roadmap source policy and attribution requirements |
|
||||
|
||||
## Editor framework
|
||||
|
||||
@ -59,7 +58,6 @@ Detailed milestone and feature plans live in [`.cursor/plans/`](../.cursor/plans
|
||||
| `rendering_unification_*.plan.md` | Unified rendering stack, Hybrid Auto GI, emissive materials |
|
||||
| `static_mesh_asset_refactor_*.plan.md` | Static mesh renderer, normalized model artifacts, inspector redesign |
|
||||
| `component_system_refactor_*.plan.md` | Component registry, imported asset refs, collider split, material overrides |
|
||||
| `jackdaw_feature_roadmap_*.plan.md` | Jackdaw-inspired production roadmap; tracked in Gitea as `BS-JD-*` issues |
|
||||
|
||||
## Crate responsibilities (quick reference)
|
||||
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
Status: Accepted
|
||||
|
||||
Context
|
||||
=======
|
||||
|
||||
Blacksite is evolving from a game-specific editor scaffold into a reusable
|
||||
Bevy editor sandbox for future game work. The Jackdaw editor provides useful
|
||||
public reference material for editor workflows such as brush geometry, material
|
||||
browser organization, terrain tools, physics placement, project launchers, and
|
||||
extension boundaries.
|
||||
|
||||
The roadmap should use those references as product and workflow inspiration
|
||||
without accidentally turning Blacksite into a fork, importing code without
|
||||
attribution, or overriding Blacksite's existing authoring/hydration model.
|
||||
|
||||
Decision
|
||||
========
|
||||
|
||||
Treat Jackdaw as an inspiration source, not as an architecture or format
|
||||
authority. Blacksite keeps its current foundations: Bevy DynamicScene RON,
|
||||
schema stamping and migration, authoring-only validation, hydration into runtime
|
||||
components, in-process PIE, and the unified viewport.
|
||||
|
||||
Each Jackdaw-inspired ticket must identify whether the work is:
|
||||
|
||||
- Design-inspired: adapted from public behavior, screenshots, docs, or workflow
|
||||
descriptions.
|
||||
- API-inspired: adapted from public API shape or concepts without copied code.
|
||||
- Code-derived: containing copied or closely translated source code.
|
||||
|
||||
Code-derived work requires a source URL, upstream commit or release reference,
|
||||
compatible license confirmation, and preserved file-level notices when required
|
||||
by the source license. Prefer clean-room implementation for geometry, terrain,
|
||||
operators, and extension APIs because Blacksite's scene and runtime ownership
|
||||
model differs from Jackdaw's.
|
||||
|
||||
Consequences
|
||||
============
|
||||
|
||||
The Jackdaw roadmap can be used for planning and prioritization while keeping
|
||||
Blacksite's architecture coherent and auditable.
|
||||
|
||||
M0-M5 planning details live in `.cursor/plans/` until implemented. Durable
|
||||
decisions move into `docs/adr/` or `docs/editor/`; user-visible workflows move
|
||||
into the root README. The Gitea issue tracker is the production task board, but
|
||||
docs remain the long-term source for accepted policy and architecture.
|
||||
|
||||
Reviewers should reject Jackdaw-inspired changes that do not declare their
|
||||
inspiration level or that copy source without license and source attribution.
|
||||
@ -34,7 +34,6 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
||||
## Design intentions (stable)
|
||||
|
||||
- **EditorOnly** helper entities never appear in hierarchy or saved scenes. Visualizer proxies are editor-only but selectable; selection resolves to their source entity.
|
||||
- **Jackdaw-inspired roadmap work** follows ADR 0020: use Jackdaw as design/API inspiration only unless a ticket explicitly marks code-derived work with source, license, and upstream commit notes. Clean-room implementation is preferred for geometry, terrain, operator, and extension systems.
|
||||
- **Dedicated egui `Camera2d`** at full window — never attach `PrimaryEguiContext` to a viewport-cropped 3D camera (NaN layout panic).
|
||||
- **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping.
|
||||
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
|
||||
@ -48,9 +47,8 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
||||
- **Player placement** is authored through `PlayerSpawn`; selecting the runtime player in Edit mode redirects to a saved `Player Start`.
|
||||
- **World lighting** has two layers: project default sun/ambient settings, and optional scene-authored directional `LightDesc` overrides. **Scene → Lighting** menu and **Rendering** panel (Window) restore project sun or bake a scene directional from project settings. **Apply** in Project Settings updates ambient and sun illuminance live.
|
||||
- **Rendering** — `GiMode` + post-process volumes; see [rendering.md](rendering.md). **Window → Rendering** shows requested/effective active-camera stack, fallback reason, Solari eligibility, viewport GI badge, and volume HUD.
|
||||
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and an Add Component footer (`actor_inspector`) that expands an inline search shelf above the button; only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. The Add Component shelf is registry-driven with persistent search, category groups, descriptions, hydration hints, duplicate/conflict disabled states, and recommendation hints. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
||||
- **Command palette** (`Ctrl+P`): focuses the filter on open, selects the first filtered command, supports arrow-key selection and Enter to run. Commands include `scene.reset_lighting`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, `selection.reset_transform`, and play commands.
|
||||
- **Editor input routing** gives egui text fields first claim on keyboard input. Viewport shortcut keys require viewport pointer focus and are suspended while RMB/MMB camera navigation is active.
|
||||
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and a pinned bottom Add Component footer (`actor_inspector`); only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
||||
- **Command palette** (`Ctrl+P`): `scene.reset_lighting`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, and play commands.
|
||||
- **ActorKind** is required on saved level objects (schema v2). Save strips hydrated ECS before writing `.scn.ron`.
|
||||
- **Scene visualizers** expose actor root icons, colliders, lights, player spawns, prefab/model anchors, Project Sun, and runtime player/camera markers without changing saved scene data. Viewport options include actor icon size and transform gizmo size sliders.
|
||||
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user