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 |
|
| `Ctrl+G` | Toggle viewport grid |
|
||||||
| `F` | Focus editor camera on selection |
|
| `F` | Focus editor camera on selection |
|
||||||
| `Ctrl+Shift+1` / `Ctrl+Shift+2` | Save / recall viewport camera bookmark |
|
| `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 + `W` `A` `S` `D` | Editor camera fly |
|
||||||
| RMB + `Q` / `E` | Editor camera down / up |
|
| RMB + `Q` / `E` | Editor camera down / up |
|
||||||
| Mouse wheel | Dolly editor camera |
|
| 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 |
|
| 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 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/` |
|
| 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 |
|
| `F7` | While paused in Play: advance one sim tick |
|
||||||
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
||||||
| Hierarchy context | Reparent to other selection / Unparent |
|
| Hierarchy context | Reparent to other selection / Unparent |
|
||||||
| File menu | New, Open, Save, Save As, Import Assets, Export Selection, Save Selection As Prefab, Recent Scenes |
|
| 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 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) |
|
| 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
|
### Play Mode
|
||||||
|
|
||||||
- Press **F5** (or Play menu) to run the **real game** in-process: same `GamePlugin`, player,
|
- 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::{
|
use crate::ui::helpers::{
|
||||||
reset_scene_lighting_to_project_defaults, toggle_play_mode, toggle_play_paused,
|
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::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM};
|
||||||
use crate::ui::UiState;
|
use crate::ui::UiState;
|
||||||
|
|
||||||
@ -127,8 +127,6 @@ pub struct CommandPalette {
|
|||||||
pub open: bool,
|
pub open: bool,
|
||||||
pub filter: String,
|
pub filter: String,
|
||||||
pub pending_run: Option<String>,
|
pub pending_run: Option<String>,
|
||||||
pub focus_filter: bool,
|
|
||||||
pub selected_index: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExtensibilityPlugin;
|
pub struct ExtensibilityPlugin;
|
||||||
@ -162,7 +160,6 @@ fn register_builtin_commands(mut registry: ResMut<EditorCommandRegistry>) {
|
|||||||
registry.register(Box::new(ResetLightingCommand));
|
registry.register(Box::new(ResetLightingCommand));
|
||||||
registry.register(Box::new(GroupSelectionCommand));
|
registry.register(Box::new(GroupSelectionCommand));
|
||||||
registry.register(Box::new(FocusSelectionCommand));
|
registry.register(Box::new(FocusSelectionCommand));
|
||||||
registry.register(Box::new(ResetSelectionTransformCommand));
|
|
||||||
registry.register(Box::new(CreatePostProcessVolumeCommand));
|
registry.register(Box::new(CreatePostProcessVolumeCommand));
|
||||||
registry.register(Box::new(FocusActiveVolumesCommand));
|
registry.register(Box::new(FocusActiveVolumesCommand));
|
||||||
registry.register(Box::new(SelectVolumesAtCameraCommand));
|
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;
|
struct CreatePostProcessVolumeCommand;
|
||||||
|
|
||||||
impl EditorCommand for 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)) {
|
if ctx.input(|input| input.modifiers.command && input.key_pressed(egui::Key::P)) {
|
||||||
palette.open = true;
|
palette.open = true;
|
||||||
palette.focus_filter = true;
|
|
||||||
palette.selected_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !palette.open {
|
if !palette.open {
|
||||||
@ -356,71 +339,25 @@ fn command_palette_ui(
|
|||||||
.color(TEXT_DIM)
|
.color(TEXT_DIM)
|
||||||
.small(),
|
.small(),
|
||||||
);
|
);
|
||||||
let filter_response = ui.add(
|
ui.add(
|
||||||
egui::TextEdit::singleline(&mut palette.filter)
|
egui::TextEdit::singleline(&mut palette.filter)
|
||||||
.hint_text("Type to filter...")
|
.hint_text("Type to filter...")
|
||||||
.desired_width(f32::INFINITY),
|
.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();
|
ui.separator();
|
||||||
egui::ScrollArea::vertical()
|
egui::ScrollArea::vertical()
|
||||||
.max_height(280.0)
|
.max_height(280.0)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for (index, name) in filtered_names.iter().enumerate() {
|
for name in registry.names() {
|
||||||
let selected = index == palette.selected_index;
|
if !palette.filter.is_empty()
|
||||||
let response =
|
&& !name.to_lowercase().contains(&palette.filter.to_lowercase())
|
||||||
ui.selectable_label(selected, egui::RichText::new(name).color(TEXT));
|
{
|
||||||
if selected {
|
continue;
|
||||||
response.scroll_to_me(Some(egui::Align::Center));
|
|
||||||
}
|
}
|
||||||
|
let response =
|
||||||
|
ui.selectable_label(false, egui::RichText::new(&name).color(TEXT));
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
palette.selected_index = index;
|
palette.pending_run = Some(name);
|
||||||
palette.pending_run = Some(name.clone());
|
|
||||||
palette.open = false;
|
palette.open = false;
|
||||||
ui.close();
|
ui.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use crate::ui::hierarchy_ops::{
|
|||||||
apply_sibling_change, apply_sibling_change_new, next_sibling_index,
|
apply_sibling_change, apply_sibling_change_new, next_sibling_index,
|
||||||
reorder_entities_under_parent, would_create_cycle,
|
reorder_entities_under_parent, would_create_cycle,
|
||||||
};
|
};
|
||||||
use crate::ui::{egui_captures_keyboard_from_world, UiState};
|
use crate::ui::UiState;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
pub use commands::{EditorCommand, EditorEntitySnapshot, SiblingChange};
|
pub use commands::{EditorCommand, EditorEntitySnapshot, SiblingChange};
|
||||||
@ -637,42 +637,9 @@ pub fn set_transform_with_history(
|
|||||||
if transform_nearly_eq(&old, &new) {
|
if transform_nearly_eq(&old, &new) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
|
||||||
*transform = new;
|
|
||||||
}
|
|
||||||
push_history(world, EditorCommand::SetTransform { entity, old, 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(
|
pub fn set_actor_kind_with_history(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
@ -1189,10 +1156,6 @@ fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_history_hotkeys(world: &mut World) {
|
fn handle_history_hotkeys(world: &mut World) {
|
||||||
if egui_captures_keyboard_from_world(world) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (undo, redo, delete, duplicate) = {
|
let (undo, redo, delete, duplicate) = {
|
||||||
let keys = world.resource::<ButtonInput<KeyCode>>();
|
let keys = world.resource::<ButtonInput<KeyCode>>();
|
||||||
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
|
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
|
||||||
@ -1256,7 +1219,7 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
|||||||
let start_group =
|
let start_group =
|
||||||
active.map(|(entity, transform)| selected_transforms(world, &selection, entity, transform));
|
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>();
|
let mut tracker = world.resource_mut::<TransformEditTracker>();
|
||||||
if tracker.active.is_none() {
|
if tracker.active.is_none() {
|
||||||
if let (Some((entity, transform)), Some(group)) = (active, start_group) {
|
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() {
|
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() => {
|
Some(state) if active.is_some() => {
|
||||||
let live = Some(state.clone());
|
|
||||||
tracker.active = Some(state);
|
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 {
|
if let Some((primary, old_primary, group)) = finished_edit {
|
||||||
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
||||||
return;
|
return;
|
||||||
@ -1293,36 +1245,34 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
|||||||
set_transform_with_history(world, primary, old_primary, new_primary);
|
set_transform_with_history(world, primary, old_primary, new_primary);
|
||||||
return;
|
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 translation_delta = new_primary.translation - old_primary.translation;
|
||||||
let rotation_delta = new_primary.rotation * old_primary.rotation.inverse();
|
let rotation_delta = new_primary.rotation * old_primary.rotation.inverse();
|
||||||
let scale_ratio = new_primary.scale / old_primary.scale;
|
let scale_ratio = new_primary.scale / old_primary.scale;
|
||||||
group
|
let mut entities = Vec::new();
|
||||||
.into_iter()
|
let mut olds = Vec::new();
|
||||||
.map(|(entity, old)| {
|
let mut news = Vec::new();
|
||||||
|
for (entity, old) in group {
|
||||||
let new = Transform {
|
let new = Transform {
|
||||||
translation: old.translation + translation_delta,
|
translation: old.translation + translation_delta,
|
||||||
rotation: rotation_delta * old.rotation,
|
rotation: rotation_delta * old.rotation,
|
||||||
scale: old.scale * scale_ratio,
|
scale: old.scale * scale_ratio,
|
||||||
};
|
};
|
||||||
(entity, old, new)
|
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||||
})
|
*transform = new;
|
||||||
.collect()
|
}
|
||||||
}
|
entities.push(entity);
|
||||||
|
olds.push(old);
|
||||||
fn apply_transform_group(world: &mut World, changes: &[(Entity, Transform, Transform)]) {
|
news.push(new);
|
||||||
for (entity, _, new) in changes {
|
}
|
||||||
if let Some(mut transform) = world.get_mut::<Transform>(*entity) {
|
if !entities.is_empty() {
|
||||||
*transform = *new;
|
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);
|
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()
|
egui::ScrollArea::vertical()
|
||||||
.auto_shrink([false, false])
|
.auto_shrink([false, false])
|
||||||
.show(ui, |ui| {
|
.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>| {
|
world.resource_scope(|world, registry: Mut<ActorInspectorSectionRegistry>| {
|
||||||
registry.draw_sections(world, ui, entity);
|
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 {
|
fn actor_icon(kind: ActorKind) -> egui_phosphor_icons::Icon {
|
||||||
|
|||||||
@ -5,11 +5,9 @@ use egui_phosphor_icons::icons;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum EditorComponentCategory {
|
pub enum EditorComponentCategory {
|
||||||
Authoring,
|
|
||||||
Rendering,
|
Rendering,
|
||||||
Physics,
|
Physics,
|
||||||
Gameplay,
|
Gameplay,
|
||||||
Volumes,
|
|
||||||
Editor,
|
Editor,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,13 +19,8 @@ pub struct EditorComponentDescriptor {
|
|||||||
pub addable: bool,
|
pub addable: bool,
|
||||||
pub removable: bool,
|
pub removable: bool,
|
||||||
pub reorderable: bool,
|
pub reorderable: bool,
|
||||||
pub hidden: bool,
|
|
||||||
pub icon: &'static str,
|
pub icon: &'static str,
|
||||||
pub description: &'static str,
|
|
||||||
pub search_terms: &'static [&'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)]
|
#[derive(Resource, Debug, Clone)]
|
||||||
@ -46,14 +39,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::CUBE.as_str(),
|
icon: icons::CUBE.as_str(),
|
||||||
description: "Renders one or more normalized imported mesh slots.",
|
search_terms: &["mesh", "renderer", "rendering", "model"],
|
||||||
search_terms: &["mesh", "renderer", "rendering", "model", "static"],
|
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &["shared::components::Primitive"],
|
|
||||||
hydration_effect:
|
|
||||||
"Hydrates into generated mesh children and material bindings.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::MaterialDesc",
|
type_name: "shared::components::MaterialDesc",
|
||||||
@ -62,13 +49,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::PALETTE.as_str(),
|
icon: icons::PALETTE.as_str(),
|
||||||
description: "Defines an authored material directly on this actor.",
|
|
||||||
search_terms: &["material", "shader", "texture"],
|
search_terms: &["material", "shader", "texture"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into Bevy material assets for renderable actors.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::RigidBodyDesc",
|
type_name: "shared::components::RigidBodyDesc",
|
||||||
@ -77,13 +59,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::SPHERE.as_str(),
|
icon: icons::SPHERE.as_str(),
|
||||||
description: "Controls authored rigid-body behavior for physics hydration.",
|
|
||||||
search_terms: &["body", "physics", "rigidbody"],
|
search_terms: &["body", "physics", "rigidbody"],
|
||||||
recommended: &["shared::components::ColliderDesc"],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into runtime Avian rigid-body state.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::ColliderDesc",
|
type_name: "shared::components::ColliderDesc",
|
||||||
@ -92,13 +69,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::SELECTION.as_str(),
|
icon: icons::SELECTION.as_str(),
|
||||||
description: "Defines authored collision shape data.",
|
|
||||||
search_terms: &["collision", "trigger", "physics"],
|
search_terms: &["collision", "trigger", "physics"],
|
||||||
recommended: &["shared::components::RigidBodyDesc"],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into runtime Avian collider state.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::LightDesc",
|
type_name: "shared::components::LightDesc",
|
||||||
@ -107,29 +79,18 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::LIGHTBULB.as_str(),
|
icon: icons::LIGHTBULB.as_str(),
|
||||||
description: "Adds authored point, spot, directional, or area lighting.",
|
search_terms: &["light", "lamp", "illumination"],
|
||||||
search_terms: &["light", "lamp", "illumination", "sun"],
|
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into runtime Bevy light components.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::Primitive",
|
type_name: "shared::components::Primitive",
|
||||||
display_name: "Primitive",
|
display_name: "Primitive",
|
||||||
category: EditorComponentCategory::Authoring,
|
category: EditorComponentCategory::Rendering,
|
||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::CUBE.as_str(),
|
icon: icons::CUBE.as_str(),
|
||||||
description: "Creates an authored built-in primitive shape.",
|
|
||||||
search_terms: &["box", "sphere", "ramp", "primitive"],
|
search_terms: &["box", "sphere", "ramp", "primitive"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &["shared::components::StaticMeshRenderer"],
|
|
||||||
hydration_effect:
|
|
||||||
"Hydrates into generated primitive render and collision data.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::PlayerSpawn",
|
type_name: "shared::components::PlayerSpawn",
|
||||||
@ -138,14 +99,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::PERSON_SIMPLE_RUN.as_str(),
|
icon: icons::PERSON_SIMPLE_RUN.as_str(),
|
||||||
description: "Marks where Play mode should spawn the player.",
|
|
||||||
search_terms: &["player", "spawn", "start"],
|
search_terms: &["player", "spawn", "start"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect:
|
|
||||||
"Used by PIE/session startup; no saved runtime player is created.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::WeaponSpawn",
|
type_name: "shared::components::WeaponSpawn",
|
||||||
@ -154,28 +109,18 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::CROSSHAIR.as_str(),
|
icon: icons::CROSSHAIR.as_str(),
|
||||||
description: "Marks a gameplay weapon spawn point.",
|
|
||||||
search_terms: &["weapon", "spawn"],
|
search_terms: &["weapon", "spawn"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::TriggerVolume",
|
type_name: "shared::components::TriggerVolume",
|
||||||
display_name: "Trigger Volume",
|
display_name: "Trigger Volume",
|
||||||
category: EditorComponentCategory::Volumes,
|
category: EditorComponentCategory::Gameplay,
|
||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::SELECTION.as_str(),
|
icon: icons::SELECTION.as_str(),
|
||||||
description: "Adds an authored gameplay trigger volume.",
|
|
||||||
search_terms: &["trigger", "volume", "event"],
|
search_terms: &["trigger", "volume", "event"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into gameplay trigger queries and visualizers.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::TeamSpawn",
|
type_name: "shared::components::TeamSpawn",
|
||||||
@ -184,13 +129,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::FLAG.as_str(),
|
icon: icons::FLAG.as_str(),
|
||||||
description: "Marks a gameplay team spawn point.",
|
|
||||||
search_terms: &["team", "spawn"],
|
search_terms: &["team", "spawn"],
|
||||||
recommended: &[],
|
|
||||||
conflicts_with: &[],
|
|
||||||
hydration_effect: "Hydrates into gameplay spawn metadata.",
|
|
||||||
},
|
},
|
||||||
EditorComponentDescriptor {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::ObjectiveMarker",
|
type_name: "shared::components::ObjectiveMarker",
|
||||||
@ -199,29 +139,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: true,
|
addable: true,
|
||||||
removable: true,
|
removable: true,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
hidden: false,
|
|
||||||
icon: icons::FLAG.as_str(),
|
icon: icons::FLAG.as_str(),
|
||||||
description: "Marks an authored gameplay objective location.",
|
|
||||||
search_terms: &["objective", "marker", "gameplay"],
|
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 {
|
EditorComponentDescriptor {
|
||||||
type_name: "shared::components::InspectorOrder",
|
type_name: "shared::components::InspectorOrder",
|
||||||
@ -230,14 +149,8 @@ impl Default for EditorComponentRegistry {
|
|||||||
addable: false,
|
addable: false,
|
||||||
removable: false,
|
removable: false,
|
||||||
reorderable: false,
|
reorderable: false,
|
||||||
hidden: true,
|
|
||||||
icon: icons::LIST_BULLETS.as_str(),
|
icon: icons::LIST_BULLETS.as_str(),
|
||||||
description: "Stores editor-only component ordering and active-state metadata.",
|
|
||||||
search_terms: &["editor", "order"],
|
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 crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
|
||||||
|
|
||||||
use super::component_registry::{
|
use super::component_registry::{EditorComponentCategory, EditorComponentRegistry};
|
||||||
EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
|
|
||||||
};
|
|
||||||
use super::dock_tabs::open_and_focus_tab;
|
use super::dock_tabs::open_and_focus_tab;
|
||||||
use super::helpers::create_scene_sun_override_from_project_settings;
|
use super::helpers::create_scene_sun_override_from_project_settings;
|
||||||
use super::{EditorTab, UiState};
|
use super::{EditorTab, UiState};
|
||||||
@ -103,22 +101,6 @@ pub(crate) struct InspectorClipboard {
|
|||||||
component: Option<CopiedComponent>,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
enum CopiedComponent {
|
enum CopiedComponent {
|
||||||
Primitive(Primitive),
|
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) {
|
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);
|
ui.add_space(4.0);
|
||||||
let button_response = egui::Frame::new()
|
egui::Frame::new()
|
||||||
.fill(WIDGET_BG)
|
.fill(WIDGET_BG)
|
||||||
.stroke(egui::Stroke::new(1.0, BORDER))
|
.stroke(egui::Stroke::new(1.0, BORDER))
|
||||||
.corner_radius(egui::CornerRadius::same(4))
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
.inner_margin(egui::Margin::symmetric(8, 6))
|
.inner_margin(egui::Margin::symmetric(8, 6))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let open_for_entity = add_component_picker_open_for(world, entity);
|
ui.horizontal_wrapped(|ui| {
|
||||||
let label = if open_for_entity {
|
let mut search = String::new();
|
||||||
"Close Add Component"
|
let search_width = fit_width(ui, MIN_INLINE_CONTROL_WIDTH, TEXT_FIELD_MAX_WIDTH);
|
||||||
} else {
|
ui.add(
|
||||||
"+ Add Component"
|
egui::TextEdit::singleline(&mut search)
|
||||||
};
|
|
||||||
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)
|
|
||||||
.hint_text("Search components...")
|
.hint_text("Search components...")
|
||||||
.desired_width(f32::INFINITY),
|
.desired_width(search_width),
|
||||||
);
|
);
|
||||||
if search_response.changed() {
|
ui.menu_button("Add Component", |ui| {
|
||||||
let mut state = world.resource_mut::<InspectorPanelState>();
|
for category in [
|
||||||
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,
|
|
||||||
EditorComponentCategory::Rendering,
|
EditorComponentCategory::Rendering,
|
||||||
EditorComponentCategory::Physics,
|
EditorComponentCategory::Physics,
|
||||||
EditorComponentCategory::Gameplay,
|
EditorComponentCategory::Gameplay,
|
||||||
EditorComponentCategory::Volumes,
|
] {
|
||||||
]
|
let mut wrote_category = false;
|
||||||
.into_iter()
|
for descriptor in descriptors.iter().filter(|descriptor| {
|
||||||
.flat_map(|category| {
|
descriptor.addable && descriptor.category == category
|
||||||
descriptors.iter().filter(move |descriptor| {
|
}) {
|
||||||
descriptor.addable
|
if !component_addable(world, entity, descriptor.type_name) {
|
||||||
&& !descriptor.hidden
|
continue;
|
||||||
&& descriptor.category == category
|
}
|
||||||
&& descriptor_matches_search(descriptor, search)
|
if !wrote_category {
|
||||||
})
|
ui.label(panel_heading(match category {
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn component_category_label(category: EditorComponentCategory) -> &'static str {
|
|
||||||
match category {
|
|
||||||
EditorComponentCategory::Authoring => "Authoring",
|
|
||||||
EditorComponentCategory::Rendering => "Rendering",
|
EditorComponentCategory::Rendering => "Rendering",
|
||||||
EditorComponentCategory::Physics => "Physics",
|
EditorComponentCategory::Physics => "Physics",
|
||||||
EditorComponentCategory::Gameplay => "Gameplay",
|
EditorComponentCategory::Gameplay => "Gameplay",
|
||||||
EditorComponentCategory::Volumes => "Volumes",
|
|
||||||
EditorComponentCategory::Editor => "Editor",
|
EditorComponentCategory::Editor => "Editor",
|
||||||
|
}));
|
||||||
|
wrote_category = true;
|
||||||
}
|
}
|
||||||
}
|
let mut clicked = false;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
struct ComponentAddState {
|
ui.label(phosphor_icon_text(descriptor.icon, 14.0).color(TEXT_DIM));
|
||||||
addable: bool,
|
clicked = ui
|
||||||
reason: Option<String>,
|
.button(descriptor.display_name)
|
||||||
recommended: Vec<&'static str>,
|
.on_hover_text(format!(
|
||||||
conflicts: Vec<&'static str>,
|
"{} | removable={} reorderable={} | {}",
|
||||||
}
|
descriptor.type_name,
|
||||||
|
descriptor.removable,
|
||||||
fn descriptor_matches_search(descriptor: &EditorComponentDescriptor, search: &str) -> bool {
|
descriptor.reorderable,
|
||||||
search.trim().is_empty()
|
descriptor.search_terms.join(", ")
|
||||||
|| 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(", ")
|
|
||||||
))
|
))
|
||||||
} else {
|
.clicked();
|
||||||
None
|
});
|
||||||
};
|
if clicked {
|
||||||
ComponentAddState {
|
insert_registered_component(world, entity, descriptor.type_name);
|
||||||
addable: reason.is_none(),
|
ui.close();
|
||||||
reason,
|
|
||||||
recommended,
|
|
||||||
conflicts,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if wrote_category {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn component_hover_text(
|
fn component_addable(world: &World, entity: Entity, type_name: &str) -> bool {
|
||||||
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 {
|
|
||||||
match type_name {
|
match type_name {
|
||||||
"shared::components::Primitive" => world.get::<Primitive>(entity).is_some(),
|
"shared::components::Primitive" => world.get::<Primitive>(entity).is_none(),
|
||||||
"shared::components::StaticMeshRenderer" => {
|
"shared::components::StaticMeshRenderer" => {
|
||||||
world.get::<StaticMeshRenderer>(entity).is_some()
|
world.get::<StaticMeshRenderer>(entity).is_none()
|
||||||
}
|
|
||||||
"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()
|
|
||||||
}
|
}
|
||||||
|
"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,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1139,11 +791,6 @@ fn insert_registered_component(world: &mut World, entity: Entity, type_name: &st
|
|||||||
objective_id: "objective".into(),
|
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>,
|
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 {
|
impl UiState {
|
||||||
pub fn default_layout() -> Self {
|
pub fn default_layout() -> Self {
|
||||||
Self::from_prefs(&UserPreferences::default())
|
Self::from_prefs(&UserPreferences::default())
|
||||||
@ -138,7 +113,6 @@ impl UiState {
|
|||||||
);
|
);
|
||||||
|
|
||||||
editor_toolbar_panel(world, ctx);
|
editor_toolbar_panel(world, ctx);
|
||||||
status_bar_ui(world, ctx, &self.selected_entities, mode);
|
|
||||||
|
|
||||||
self.pointer_in_viewport = false;
|
self.pointer_in_viewport = false;
|
||||||
self.viewport_pointer_pos = None;
|
self.viewport_pointer_pos = None;
|
||||||
@ -180,6 +154,8 @@ impl UiState {
|
|||||||
);
|
);
|
||||||
tick_layout_save(world, dt);
|
tick_layout_save(world, dt);
|
||||||
|
|
||||||
|
status_bar_ui(world, ctx, &self.selected_entities, mode);
|
||||||
|
|
||||||
let mut diagnostics_open = world.resource::<DiagnosticsPanel>().open;
|
let mut diagnostics_open = world.resource::<DiagnosticsPanel>().open;
|
||||||
diagnostics_window(world, ctx, &mut diagnostics_open);
|
diagnostics_window(world, ctx, &mut diagnostics_open);
|
||||||
world.resource_mut::<DiagnosticsPanel>().open = 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::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
|
||||||
.init_resource::<asset_browser::AssetBrowserUiState>()
|
.init_resource::<asset_browser::AssetBrowserUiState>()
|
||||||
.init_resource::<inspector::InspectorClipboard>()
|
.init_resource::<inspector::InspectorClipboard>()
|
||||||
.init_resource::<inspector::InspectorPanelState>()
|
|
||||||
.init_resource::<ViewportUiState>()
|
.init_resource::<ViewportUiState>()
|
||||||
.init_resource::<LayoutSaveTimer>()
|
.init_resource::<LayoutSaveTimer>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
|
|||||||
@ -5,11 +5,8 @@ use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
|||||||
use shared::LevelObject;
|
use shared::LevelObject;
|
||||||
|
|
||||||
use crate::camera::EditorCamera;
|
use crate::camera::EditorCamera;
|
||||||
use crate::history::{
|
use crate::history::{delete_entities_with_history, duplicate_entities_with_history};
|
||||||
delete_entities_with_history, duplicate_entities_with_history, set_transform_group_with_history,
|
|
||||||
};
|
|
||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::ui::UiState;
|
|
||||||
|
|
||||||
pub fn focus_editor_camera_on_selection(world: &mut World) {
|
pub fn focus_editor_camera_on_selection(world: &mut World) {
|
||||||
let Some(entity) = world.resource::<SelectedEntity>().0 else {
|
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);
|
let entities = selected_level_entities(world, selected);
|
||||||
duplicate_entities_with_history(world, &entities);
|
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));
|
.filter(|pos| rect.contains(*pos));
|
||||||
*pointer_in_viewport = viewport_pointer_pos.is_some();
|
*pointer_in_viewport = viewport_pointer_pos.is_some();
|
||||||
|
|
||||||
let primary_clicked = ui.input(|input| {
|
let primary_clicked =
|
||||||
input.pointer.button_clicked(egui::PointerButton::Primary) && !input.modifiers.alt
|
ui.input(|input| input.pointer.button_clicked(egui::PointerButton::Primary));
|
||||||
});
|
|
||||||
if primary_clicked && !clean_game_view {
|
if primary_clicked && !clean_game_view {
|
||||||
if let Some(pos) = response
|
if let Some(pos) = response
|
||||||
.interact_pointer_pos()
|
.interact_pointer_pos()
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
use bevy::camera::visibility::RenderLayers;
|
use bevy::camera::visibility::RenderLayers;
|
||||||
use bevy::input::mouse::{AccumulatedMouseMotion, MouseWheel};
|
use bevy::input::mouse::{AccumulatedMouseMotion, MouseWheel};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::{CursorOptions, PrimaryWindow};
|
|
||||||
use bevy_egui::{EguiContexts, EguiGlobalSettings, PrimaryEguiContext};
|
use bevy_egui::{EguiContexts, EguiGlobalSettings, PrimaryEguiContext};
|
||||||
use transform_gizmo_bevy::prelude::GizmoCamera;
|
use transform_gizmo_bevy::prelude::GizmoCamera;
|
||||||
|
|
||||||
use crate::infra::EditorOnly;
|
use crate::infra::EditorOnly;
|
||||||
use crate::state::scene_tools_active;
|
use crate::state::scene_tools_active;
|
||||||
use crate::ui::{egui_captures_keyboard, UiState};
|
use crate::ui::UiState;
|
||||||
use settings::ProjectSettings;
|
use settings::ProjectSettings;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
@ -29,24 +28,9 @@ impl Default for EditorCamera {
|
|||||||
|
|
||||||
pub struct EditorCameraPlugin;
|
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 {
|
impl Plugin for EditorCameraPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<EditorCameraInputState>()
|
app.add_systems(Startup, spawn_editor_camera).add_systems(
|
||||||
.add_systems(Startup, spawn_editor_camera)
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
Update,
|
||||||
(sync_editor_camera_from_settings, update_editor_camera)
|
(sync_editor_camera_from_settings, update_editor_camera)
|
||||||
.chain()
|
.chain()
|
||||||
@ -120,61 +104,25 @@ fn update_editor_camera(
|
|||||||
mut scroll: MessageReader<MouseWheel>,
|
mut scroll: MessageReader<MouseWheel>,
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
ui_state: Res<UiState>,
|
ui_state: Res<UiState>,
|
||||||
mut input_state: ResMut<EditorCameraInputState>,
|
|
||||||
mut cameras: Query<(&mut Transform, &EditorCamera)>,
|
mut cameras: Query<(&mut Transform, &EditorCamera)>,
|
||||||
mut cursor: Single<&mut CursorOptions, With<PrimaryWindow>>,
|
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let ctx = contexts.ctx_mut()?;
|
let ctx = contexts.ctx_mut()?;
|
||||||
let in_scene = ui_state.pointer_in_viewport;
|
let in_scene = ui_state.pointer_in_viewport;
|
||||||
let navigation_pressed =
|
if ctx.wants_keyboard_input() {
|
||||||
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);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if in_scene && buttons.just_pressed(MouseButton::Right) {
|
if !in_scene && ctx.wants_pointer_input() {
|
||||||
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 {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if !in_scene && input_state.mode.is_none() && ctx.wants_pointer_input() {
|
if in_scene && ctx.wants_pointer_input() && !buttons.pressed(MouseButton::Right) {
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if in_scene
|
|
||||||
&& input_state.mode.is_none()
|
|
||||||
&& ctx.wants_pointer_input()
|
|
||||||
&& !buttons.pressed(MouseButton::Right)
|
|
||||||
{
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let dt = time.delta_secs();
|
let dt = time.delta_secs();
|
||||||
let raw_scroll_delta: f32 = scroll.read().map(|event| event.y).sum();
|
let 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
|
|
||||||
};
|
|
||||||
|
|
||||||
for (mut transform, camera) in &mut cameras {
|
for (mut transform, camera) in &mut cameras {
|
||||||
let looking = matches!(input_state.mode, Some(CameraNavigationMode::Look))
|
if buttons.pressed(MouseButton::Right) {
|
||||||
&& buttons.pressed(MouseButton::Right);
|
|
||||||
let panning = matches!(input_state.mode, Some(CameraNavigationMode::Pan))
|
|
||||||
&& buttons.pressed(MouseButton::Middle);
|
|
||||||
|
|
||||||
if looking {
|
|
||||||
let delta = mouse_motion.delta;
|
let delta = mouse_motion.delta;
|
||||||
if delta != Vec2::ZERO {
|
if delta != Vec2::ZERO {
|
||||||
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
|
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
|
||||||
@ -192,7 +140,6 @@ fn update_editor_camera(
|
|||||||
let up = Vec3::Y;
|
let up = Vec3::Y;
|
||||||
let mut wish = Vec3::ZERO;
|
let mut wish = Vec3::ZERO;
|
||||||
|
|
||||||
if looking {
|
|
||||||
if keys.pressed(KeyCode::KeyW) {
|
if keys.pressed(KeyCode::KeyW) {
|
||||||
wish += forward;
|
wish += forward;
|
||||||
}
|
}
|
||||||
@ -211,7 +158,6 @@ fn update_editor_camera(
|
|||||||
if keys.pressed(KeyCode::KeyQ) {
|
if keys.pressed(KeyCode::KeyQ) {
|
||||||
wish -= up;
|
wish -= up;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let sprint = if keys.pressed(KeyCode::ShiftLeft) {
|
let sprint = if keys.pressed(KeyCode::ShiftLeft) {
|
||||||
3.0
|
3.0
|
||||||
@ -221,7 +167,7 @@ fn update_editor_camera(
|
|||||||
transform.translation += wish.normalize_or_zero() * camera.fly_speed * sprint * dt;
|
transform.translation += wish.normalize_or_zero() * camera.fly_speed * sprint * dt;
|
||||||
transform.translation += forward * scroll_delta * camera.fly_speed * 0.08;
|
transform.translation += forward * scroll_delta * camera.fly_speed * 0.08;
|
||||||
|
|
||||||
if panning {
|
if buttons.pressed(MouseButton::Middle) {
|
||||||
let delta = mouse_motion.delta;
|
let delta = mouse_motion.delta;
|
||||||
let distance_scale = transform.translation.length().max(1.0).sqrt();
|
let distance_scale = transform.translation.length().max(1.0).sqrt();
|
||||||
transform.translation +=
|
transform.translation +=
|
||||||
@ -231,13 +177,3 @@ fn update_editor_camera(
|
|||||||
|
|
||||||
Ok(())
|
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::math::Rect;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_egui::EguiContexts;
|
|
||||||
use transform_gizmo_bevy::prelude::{GizmoMode, GizmoOptions, GizmoOrientation};
|
use transform_gizmo_bevy::prelude::{GizmoMode, GizmoOptions, GizmoOrientation};
|
||||||
|
|
||||||
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
|
use crate::ui::UiState;
|
||||||
use crate::viewport::ViewportDisplayMode;
|
use crate::viewport::ViewportDisplayMode;
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -41,19 +40,12 @@ impl Plugin for EditorGizmoPlugin {
|
|||||||
|
|
||||||
fn handle_gizmo_hotkeys(
|
fn handle_gizmo_hotkeys(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
mut contexts: EguiContexts,
|
|
||||||
ui_state: Res<UiState>,
|
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
mut mode: ResMut<EditorGizmoMode>,
|
mut mode: ResMut<EditorGizmoMode>,
|
||||||
mut space: ResMut<EditorGizmoSpace>,
|
mut space: ResMut<EditorGizmoSpace>,
|
||||||
) -> Result {
|
) {
|
||||||
if display.clean_game_view {
|
if display.clean_game_view {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
|
||||||
let ctx = contexts.ctx_mut()?;
|
|
||||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
if keys.just_pressed(KeyCode::KeyW) {
|
if keys.just_pressed(KeyCode::KeyW) {
|
||||||
*mode = EditorGizmoMode::Translate;
|
*mode = EditorGizmoMode::Translate;
|
||||||
@ -70,7 +62,6 @@ fn handle_gizmo_hotkeys(
|
|||||||
EditorGizmoSpace::Local => EditorGizmoSpace::World,
|
EditorGizmoSpace::Local => EditorGizmoSpace::World,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the viewport egui panel to gizmo cursor space (required for render-to-texture).
|
/// Maps the viewport egui panel to gizmo cursor space (required for render-to-texture).
|
||||||
|
|||||||
@ -3,11 +3,10 @@
|
|||||||
use crate::camera::EditorCamera;
|
use crate::camera::EditorCamera;
|
||||||
use crate::selection::SelectedEntity;
|
use crate::selection::SelectedEntity;
|
||||||
use crate::state::scene_tools_active;
|
use crate::state::scene_tools_active;
|
||||||
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
|
|
||||||
use crate::viewport::viewport_mode::EditorViewportModePlugin;
|
use crate::viewport::viewport_mode::EditorViewportModePlugin;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::Window;
|
use bevy::window::Window;
|
||||||
use bevy_egui::{egui, EguiContexts};
|
use bevy_egui::egui;
|
||||||
|
|
||||||
/// Viewport display and snapping options (editor-only).
|
/// Viewport display and snapping options (editor-only).
|
||||||
#[derive(Resource, Debug, Clone)]
|
#[derive(Resource, Debug, Clone)]
|
||||||
@ -156,29 +155,22 @@ pub fn snap_translation(position: Vec3, settings: &ViewportSettings) -> Vec3 {
|
|||||||
|
|
||||||
fn focus_selection_hotkey(
|
fn focus_selection_hotkey(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
mut contexts: EguiContexts,
|
|
||||||
ui_state: Res<UiState>,
|
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
selected: Res<SelectedEntity>,
|
selected: Res<SelectedEntity>,
|
||||||
mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
|
mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
|
||||||
targets: Query<&GlobalTransform>,
|
targets: Query<&GlobalTransform>,
|
||||||
) -> Result {
|
) {
|
||||||
if display.clean_game_view {
|
if display.clean_game_view {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
|
||||||
let ctx = contexts.ctx_mut()?;
|
|
||||||
if !viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
if !keys.just_pressed(KeyCode::KeyF) {
|
if !keys.just_pressed(KeyCode::KeyF) {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
let Some(entity) = selected.0 else {
|
let Some(entity) = selected.0 else {
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
let Ok(target) = targets.get(entity) else {
|
let Ok(target) = targets.get(entity) else {
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
let center = target.translation();
|
let center = target.translation();
|
||||||
for mut transform in &mut editor_cameras {
|
for mut transform in &mut editor_cameras {
|
||||||
@ -187,21 +179,13 @@ fn focus_selection_hotkey(
|
|||||||
transform.translation = center - transform.forward().as_vec3() * distance;
|
transform.translation = center - transform.forward().as_vec3() * distance;
|
||||||
transform.look_at(center, Vec3::Y);
|
transform.look_at(center, Vec3::Y);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_viewport_hotkeys(
|
fn toggle_viewport_hotkeys(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
mut contexts: EguiContexts,
|
|
||||||
ui_state: Res<UiState>,
|
|
||||||
mut settings: ResMut<ViewportSettings>,
|
mut settings: ResMut<ViewportSettings>,
|
||||||
mut display: ResMut<ViewportDisplayMode>,
|
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) {
|
if keys.just_pressed(KeyCode::KeyG) {
|
||||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||||
if ctrl {
|
if ctrl {
|
||||||
@ -210,26 +194,18 @@ fn toggle_viewport_hotkeys(
|
|||||||
display.clean_game_view = !display.clean_game_view;
|
display.clean_game_view = !display.clean_game_view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn camera_bookmark_hotkeys(
|
fn camera_bookmark_hotkeys(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
mut contexts: EguiContexts,
|
|
||||||
ui_state: Res<UiState>,
|
|
||||||
scene_io: Res<crate::scene_io::SceneIo>,
|
scene_io: Res<crate::scene_io::SceneIo>,
|
||||||
mut bookmarks: ResMut<CameraBookmarks>,
|
mut bookmarks: ResMut<CameraBookmarks>,
|
||||||
mut editor_cameras: Query<&mut Transform, With<EditorCamera>>,
|
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 shift = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
|
||||||
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
let ctrl = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
|
||||||
if !shift || !ctrl {
|
if !shift || !ctrl {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scene_key = scene_io
|
let scene_key = scene_io
|
||||||
@ -240,20 +216,19 @@ fn camera_bookmark_hotkeys(
|
|||||||
|
|
||||||
if keys.just_pressed(KeyCode::Digit1) {
|
if keys.just_pressed(KeyCode::Digit1) {
|
||||||
let Ok(transform) = editor_cameras.single() else {
|
let Ok(transform) = editor_cameras.single() else {
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
save_camera_bookmark(&mut bookmarks, &scene_key, *transform);
|
save_camera_bookmark(&mut bookmarks, &scene_key, *transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.just_pressed(KeyCode::Digit2) {
|
if keys.just_pressed(KeyCode::Digit2) {
|
||||||
let Some(saved) = recall_camera_bookmark(&bookmarks, &scene_key) else {
|
let Some(saved) = recall_camera_bookmark(&bookmarks, &scene_key) else {
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
for mut transform in &mut editor_cameras {
|
for mut transform in &mut editor_cameras {
|
||||||
*transform = saved;
|
*transform = saved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_viewport_grid(
|
fn draw_viewport_grid(
|
||||||
|
|||||||
@ -92,26 +92,22 @@ fn handle_pick_events(
|
|||||||
proxies: Query<&EditorVisualizerProxy>,
|
proxies: Query<&EditorVisualizerProxy>,
|
||||||
icon_proxies: Query<&ActorIconProxy>,
|
icon_proxies: Query<&ActorIconProxy>,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
gizmo_targets: Query<&GizmoTarget>,
|
gizmo_targets: Query<&GizmoTarget>,
|
||||||
hierarchy: Option<Res<HierarchyPanelState>>,
|
hierarchy: Option<Res<HierarchyPanelState>>,
|
||||||
) -> Result {
|
) {
|
||||||
if ui_state.pointer_in_viewport
|
if keys.just_pressed(KeyCode::Escape) {
|
||||||
&& !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
|
|
||||||
&& keys.just_pressed(KeyCode::Escape)
|
|
||||||
{
|
|
||||||
ui_state.selected_entities.clear();
|
ui_state.selected_entities.clear();
|
||||||
selected.0 = None;
|
selected.0 = None;
|
||||||
pick_stack.entities.clear();
|
pick_stack.entities.clear();
|
||||||
pick_stack.index = 0;
|
pick_stack.index = 0;
|
||||||
pick_stack.anchor = None;
|
pick_stack.anchor = None;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if display.clean_game_view {
|
if display.clean_game_view {
|
||||||
viewport_click.0 = None;
|
viewport_click.0 = None;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if gizmo_targets
|
if gizmo_targets
|
||||||
@ -119,7 +115,7 @@ fn handle_pick_events(
|
|||||||
.any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
|
.any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
|
||||||
{
|
{
|
||||||
viewport_click.0 = None;
|
viewport_click.0 = None;
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let additive = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]);
|
let additive = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]);
|
||||||
@ -138,11 +134,11 @@ fn handle_pick_events(
|
|||||||
pointer_pos,
|
pointer_pos,
|
||||||
additive,
|
additive,
|
||||||
);
|
);
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ui_state.pointer_in_viewport {
|
if !ui_state.pointer_in_viewport {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for event in click_events.read() {
|
for event in click_events.read() {
|
||||||
@ -191,7 +187,6 @@ fn handle_pick_events(
|
|||||||
additive,
|
additive,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cycle_overlapping_viewport_pick(
|
fn cycle_overlapping_viewport_pick(
|
||||||
@ -199,29 +194,25 @@ fn cycle_overlapping_viewport_pick(
|
|||||||
mut selected: ResMut<SelectedEntity>,
|
mut selected: ResMut<SelectedEntity>,
|
||||||
mut pick_stack: ResMut<ViewportPickStack>,
|
mut pick_stack: ResMut<ViewportPickStack>,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
) -> Result {
|
) {
|
||||||
if display.clean_game_view {
|
if display.clean_game_view {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
|
||||||
if !ui_state.pointer_in_viewport
|
|
||||||
|| buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
if !keys.just_pressed(KeyCode::Tab) || pick_stack.entities.len() < 2 {
|
if !keys.just_pressed(KeyCode::Tab) || pick_stack.entities.len() < 2 {
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
if keys.any_pressed([KeyCode::ControlLeft, KeyCode::ShiftLeft]) {
|
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();
|
pick_stack.index = (pick_stack.index + 1) % pick_stack.entities.len();
|
||||||
let entity = pick_stack.entities[pick_stack.index];
|
let entity = pick_stack.entities[pick_stack.index];
|
||||||
ui_state.selected_entities.select_replace(entity);
|
ui_state.selected_entities.select_replace(entity);
|
||||||
selected.0 = Some(entity);
|
selected.0 = Some(entity);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_viewport_pick(
|
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 |
|
| [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 |
|
| [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 |
|
| [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
|
## 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 |
|
| `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 |
|
| `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 |
|
| `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)
|
## 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)
|
## 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.
|
- **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).
|
- **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.
|
- **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.
|
- **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`.
|
- **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.
|
- **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.
|
- **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()`.
|
- **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`): 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.
|
- **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.
|
||||||
- **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.
|
|
||||||
- **ActorKind** is required on saved level objects (schema v2). Save strips hydrated ECS before writing `.scn.ron`.
|
- **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.
|
- **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.
|
- **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