Start M0 editor workflow polish
This commit is contained in:
parent
3e053a81ab
commit
19c4d017dc
16
.github/pull_request_template.md
vendored
Normal file
16
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
## 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.
|
||||
@ -117,7 +117,7 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
||||
| Select Project Sun | Inspect project default lighting; create a scene sun override |
|
||||
| Asset Browser project/file views | Browse `assets/`, search/filter/sort, switch grid/list, expand model subassets with generated thumbnails, inspect staged import/material settings, drag assets/submeshes into the viewport |
|
||||
| Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` |
|
||||
| `Ctrl+P` | Command palette (`scene.reset_lighting`, `selection.group`, `selection.focus`, play commands) |
|
||||
| `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) |
|
||||
| `F7` | While paused in Play: advance one sim tick |
|
||||
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
||||
| Hierarchy context | Reparent to other selection / Unparent |
|
||||
|
||||
@ -8,7 +8,7 @@ use crate::state::{EditorMode, PlayPossession};
|
||||
use crate::ui::helpers::{
|
||||
reset_scene_lighting_to_project_defaults, toggle_play_mode, toggle_play_paused,
|
||||
};
|
||||
use crate::ui::selection_ops::focus_editor_camera_on_selection;
|
||||
use crate::ui::selection_ops::{focus_editor_camera_on_selection, reset_selected_transforms};
|
||||
use crate::ui::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM};
|
||||
use crate::ui::UiState;
|
||||
|
||||
@ -127,6 +127,8 @@ pub struct CommandPalette {
|
||||
pub open: bool,
|
||||
pub filter: String,
|
||||
pub pending_run: Option<String>,
|
||||
pub focus_filter: bool,
|
||||
pub selected_index: usize,
|
||||
}
|
||||
|
||||
pub struct ExtensibilityPlugin;
|
||||
@ -160,6 +162,7 @@ fn register_builtin_commands(mut registry: ResMut<EditorCommandRegistry>) {
|
||||
registry.register(Box::new(ResetLightingCommand));
|
||||
registry.register(Box::new(GroupSelectionCommand));
|
||||
registry.register(Box::new(FocusSelectionCommand));
|
||||
registry.register(Box::new(ResetSelectionTransformCommand));
|
||||
registry.register(Box::new(CreatePostProcessVolumeCommand));
|
||||
registry.register(Box::new(FocusActiveVolumesCommand));
|
||||
registry.register(Box::new(SelectVolumesAtCameraCommand));
|
||||
@ -272,6 +275,18 @@ impl EditorCommand for FocusSelectionCommand {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetSelectionTransformCommand;
|
||||
|
||||
impl EditorCommand for ResetSelectionTransformCommand {
|
||||
fn name(&self) -> &str {
|
||||
"selection.reset_transform"
|
||||
}
|
||||
|
||||
fn execute(&self, world: &mut World) {
|
||||
reset_selected_transforms(world);
|
||||
}
|
||||
}
|
||||
|
||||
struct CreatePostProcessVolumeCommand;
|
||||
|
||||
impl EditorCommand for CreatePostProcessVolumeCommand {
|
||||
@ -322,6 +337,8 @@ fn command_palette_ui(
|
||||
|
||||
if ctx.input(|input| input.modifiers.command && input.key_pressed(egui::Key::P)) {
|
||||
palette.open = true;
|
||||
palette.focus_filter = true;
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
if !palette.open {
|
||||
@ -339,25 +356,71 @@ fn command_palette_ui(
|
||||
.color(TEXT_DIM)
|
||||
.small(),
|
||||
);
|
||||
ui.add(
|
||||
let filter_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut palette.filter)
|
||||
.hint_text("Type to filter...")
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
if palette.focus_filter {
|
||||
filter_response.request_focus();
|
||||
palette.focus_filter = false;
|
||||
}
|
||||
if filter_response.changed() {
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
let filter = palette.filter.to_lowercase();
|
||||
let filtered_names: Vec<String> = registry
|
||||
.names()
|
||||
.into_iter()
|
||||
.filter(|name| filter.is_empty() || name.to_lowercase().contains(&filter))
|
||||
.collect();
|
||||
if !filtered_names.is_empty() {
|
||||
palette.selected_index = palette.selected_index.min(filtered_names.len() - 1);
|
||||
} else {
|
||||
palette.selected_index = 0;
|
||||
}
|
||||
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
|
||||
&& !filtered_names.is_empty()
|
||||
{
|
||||
palette.selected_index = (palette.selected_index + 1) % filtered_names.len();
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
|
||||
&& !filtered_names.is_empty()
|
||||
{
|
||||
palette.selected_index = if palette.selected_index == 0 {
|
||||
filtered_names.len() - 1
|
||||
} else {
|
||||
palette.selected_index - 1
|
||||
};
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
|
||||
if let Some(name) = filtered_names.get(palette.selected_index) {
|
||||
palette.pending_run = Some(name.clone());
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(280.0)
|
||||
.show(ui, |ui| {
|
||||
for name in registry.names() {
|
||||
if !palette.filter.is_empty()
|
||||
&& !name.to_lowercase().contains(&palette.filter.to_lowercase())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (index, name) in filtered_names.iter().enumerate() {
|
||||
let selected = index == palette.selected_index;
|
||||
let response =
|
||||
ui.selectable_label(false, egui::RichText::new(&name).color(TEXT));
|
||||
ui.selectable_label(selected, egui::RichText::new(name).color(TEXT));
|
||||
if selected {
|
||||
response.scroll_to_me(Some(egui::Align::Center));
|
||||
}
|
||||
if response.clicked() {
|
||||
palette.pending_run = Some(name);
|
||||
palette.selected_index = index;
|
||||
palette.pending_run = Some(name.clone());
|
||||
palette.open = false;
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@ -637,9 +637,42 @@ pub fn set_transform_with_history(
|
||||
if transform_nearly_eq(&old, &new) {
|
||||
return;
|
||||
}
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
*transform = new;
|
||||
}
|
||||
push_history(world, EditorCommand::SetTransform { entity, old, new });
|
||||
}
|
||||
|
||||
pub fn set_transform_group_with_history(
|
||||
world: &mut World,
|
||||
changes: impl IntoIterator<Item = (Entity, Transform, Transform)>,
|
||||
) {
|
||||
let mut entities = Vec::new();
|
||||
let mut olds = Vec::new();
|
||||
let mut news = Vec::new();
|
||||
for (entity, old, new) in changes {
|
||||
if transform_nearly_eq(&old, &new) {
|
||||
continue;
|
||||
}
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(entity) {
|
||||
*transform = new;
|
||||
entities.push(entity);
|
||||
olds.push(old);
|
||||
news.push(new);
|
||||
}
|
||||
}
|
||||
if !entities.is_empty() {
|
||||
push_history(
|
||||
world,
|
||||
EditorCommand::SetTransformGroup {
|
||||
entities,
|
||||
olds,
|
||||
news,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_actor_kind_with_history(
|
||||
world: &mut World,
|
||||
entity: Entity,
|
||||
@ -1219,7 +1252,7 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
let start_group =
|
||||
active.map(|(entity, transform)| selected_transforms(world, &selection, entity, transform));
|
||||
|
||||
let finished_edit = {
|
||||
let (finished_edit, live_edit) = {
|
||||
let mut tracker = world.resource_mut::<TransformEditTracker>();
|
||||
if tracker.active.is_none() {
|
||||
if let (Some((entity, transform)), Some(group)) = (active, start_group) {
|
||||
@ -1228,15 +1261,26 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
}
|
||||
}
|
||||
match tracker.active.take() {
|
||||
Some((primary, old, group)) if active.is_none() => Some((primary, old, group)),
|
||||
Some((primary, old, group)) if active.is_none() => (Some((primary, old, group)), None),
|
||||
Some(state) if active.is_some() => {
|
||||
let live = Some(state.clone());
|
||||
tracker.active = Some(state);
|
||||
None
|
||||
(None, live)
|
||||
}
|
||||
_ => None,
|
||||
_ => (None, None),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((primary, old_primary, group)) = live_edit {
|
||||
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
||||
return;
|
||||
};
|
||||
if group.len() > 1 {
|
||||
let changes = transform_group_changes(old_primary, new_primary, group);
|
||||
apply_transform_group(world, &changes);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((primary, old_primary, group)) = finished_edit {
|
||||
let Some(new_primary) = world.get::<Transform>(primary).copied() else {
|
||||
return;
|
||||
@ -1245,34 +1289,36 @@ fn capture_gizmo_transform_edits(world: &mut World) {
|
||||
set_transform_with_history(world, primary, old_primary, new_primary);
|
||||
return;
|
||||
}
|
||||
let translation_delta = new_primary.translation - old_primary.translation;
|
||||
let rotation_delta = new_primary.rotation * old_primary.rotation.inverse();
|
||||
let scale_ratio = new_primary.scale / old_primary.scale;
|
||||
let mut entities = Vec::new();
|
||||
let mut olds = Vec::new();
|
||||
let mut news = Vec::new();
|
||||
for (entity, old) in group {
|
||||
let changes = transform_group_changes(old_primary, new_primary, group);
|
||||
set_transform_group_with_history(world, changes);
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_group_changes(
|
||||
old_primary: Transform,
|
||||
new_primary: Transform,
|
||||
group: Vec<(Entity, Transform)>,
|
||||
) -> Vec<(Entity, Transform, Transform)> {
|
||||
let translation_delta = new_primary.translation - old_primary.translation;
|
||||
let rotation_delta = new_primary.rotation * old_primary.rotation.inverse();
|
||||
let scale_ratio = new_primary.scale / old_primary.scale;
|
||||
group
|
||||
.into_iter()
|
||||
.map(|(entity, old)| {
|
||||
let new = Transform {
|
||||
translation: old.translation + translation_delta,
|
||||
rotation: rotation_delta * old.rotation,
|
||||
scale: old.scale * scale_ratio,
|
||||
};
|
||||
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,
|
||||
},
|
||||
);
|
||||
(entity, old, new)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_transform_group(world: &mut World, changes: &[(Entity, Transform, Transform)]) {
|
||||
for (entity, _, new) in changes {
|
||||
if let Some(mut transform) = world.get_mut::<Transform>(*entity) {
|
||||
*transform = *new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,11 @@ use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
||||
use shared::LevelObject;
|
||||
|
||||
use crate::camera::EditorCamera;
|
||||
use crate::history::{delete_entities_with_history, duplicate_entities_with_history};
|
||||
use crate::history::{
|
||||
delete_entities_with_history, duplicate_entities_with_history, set_transform_group_with_history,
|
||||
};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::UiState;
|
||||
|
||||
pub fn focus_editor_camera_on_selection(world: &mut World) {
|
||||
let Some(entity) = world.resource::<SelectedEntity>().0 else {
|
||||
@ -58,3 +61,19 @@ pub fn duplicate_selection(world: &mut World, selected: &SelectedEntities) {
|
||||
let entities = selected_level_entities(world, selected);
|
||||
duplicate_entities_with_history(world, &entities);
|
||||
}
|
||||
|
||||
pub fn reset_selected_transforms(world: &mut World) {
|
||||
let changes: Vec<_> = world
|
||||
.resource::<UiState>()
|
||||
.selected_entities
|
||||
.iter()
|
||||
.filter(|entity| is_level_object(world, *entity))
|
||||
.filter_map(|entity| {
|
||||
world
|
||||
.get::<Transform>(entity)
|
||||
.copied()
|
||||
.map(|old| (entity, old, Transform::default()))
|
||||
})
|
||||
.collect();
|
||||
set_transform_group_with_history(world, changes);
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
||||
| [0017](adr/0017-normalized-static-mesh-assets.md) | Normalized static mesh assets and renderer placement |
|
||||
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
||||
| [0019](adr/0019-local-bevy-render-timeout-patch.md) | Local `bevy_render` patch for transient Linux swapchain timeouts |
|
||||
| [0020](adr/0020-jackdaw-inspired-editor-roadmap.md) | Jackdaw-inspired roadmap source policy and attribution requirements |
|
||||
|
||||
## Editor framework
|
||||
|
||||
@ -58,6 +59,7 @@ Detailed milestone and feature plans live in [`.cursor/plans/`](../.cursor/plans
|
||||
| `rendering_unification_*.plan.md` | Unified rendering stack, Hybrid Auto GI, emissive materials |
|
||||
| `static_mesh_asset_refactor_*.plan.md` | Static mesh renderer, normalized model artifacts, inspector redesign |
|
||||
| `component_system_refactor_*.plan.md` | Component registry, imported asset refs, collider split, material overrides |
|
||||
| `jackdaw_feature_roadmap_*.plan.md` | Jackdaw-inspired production roadmap; tracked in Gitea as `BS-JD-*` issues |
|
||||
|
||||
## Crate responsibilities (quick reference)
|
||||
|
||||
|
||||
49
docs/adr/0020-jackdaw-inspired-editor-roadmap.md
Normal file
49
docs/adr/0020-jackdaw-inspired-editor-roadmap.md
Normal file
@ -0,0 +1,49 @@
|
||||
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,6 +34,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
||||
## Design intentions (stable)
|
||||
|
||||
- **EditorOnly** helper entities never appear in hierarchy or saved scenes. Visualizer proxies are editor-only but selectable; selection resolves to their source entity.
|
||||
- **Jackdaw-inspired roadmap work** follows ADR 0020: use Jackdaw as design/API inspiration only unless a ticket explicitly marks code-derived work with source, license, and upstream commit notes. Clean-room implementation is preferred for geometry, terrain, operator, and extension systems.
|
||||
- **Dedicated egui `Camera2d`** at full window — never attach `PrimaryEguiContext` to a viewport-cropped 3D camera (NaN layout panic).
|
||||
- **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping.
|
||||
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
|
||||
@ -48,7 +49,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
||||
- **World lighting** has two layers: project default sun/ambient settings, and optional scene-authored directional `LightDesc` overrides. **Scene → Lighting** menu and **Rendering** panel (Window) restore project sun or bake a scene directional from project settings. **Apply** in Project Settings updates ambient and sun illuminance live.
|
||||
- **Rendering** — `GiMode` + post-process volumes; see [rendering.md](rendering.md). **Window → Rendering** shows requested/effective active-camera stack, fallback reason, Solari eligibility, viewport GI badge, and volume HUD.
|
||||
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and a pinned bottom Add Component footer (`actor_inspector`); only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
||||
- **Command palette** (`Ctrl+P`): `scene.reset_lighting`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, and play commands.
|
||||
- **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.
|
||||
- **ActorKind** is required on saved level objects (schema v2). Save strips hydrated ECS before writing `.scn.ron`.
|
||||
- **Scene visualizers** expose actor root icons, colliders, lights, player spawns, prefab/model anchors, Project Sun, and runtime player/camera markers without changing saved scene data. Viewport options include actor icon size and transform gizmo size sliders.
|
||||
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user