This commit is contained in:
parent
9f95ba3082
commit
fcd34f9079
@ -110,6 +110,8 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
|||||||
| `F2` in Hierarchy | Rename selection |
|
| `F2` in Hierarchy | Rename selection |
|
||||||
| `W` / `E` / `R` | Translate / rotate / scale gizmo |
|
| `W` / `E` / `R` | Translate / rotate / scale gizmo |
|
||||||
| `X` | Toggle world/local gizmo orientation |
|
| `X` | Toggle world/local gizmo orientation |
|
||||||
|
| `B` | Enter Draw Brush mode |
|
||||||
|
| Draw Brush: LMB / `Enter` / `Esc` / `Backspace` | Place floor points / create brush / cancel / remove last point |
|
||||||
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
||||||
| Viewport eye/options | Toggle actor root icon categories, adjust icon/gizmo size, and control colliders, lights, spawns, prefab/model anchors, and runtime player/camera visualizers |
|
| Viewport eye/options | Toggle actor root icon categories, adjust icon/gizmo size, and control colliders, lights, spawns, prefab/model anchors, and runtime player/camera visualizers |
|
||||||
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
||||||
@ -183,6 +185,8 @@ Viewport shortcut keys require the pointer to be in the viewport and are suspend
|
|||||||
subasset places that part through the same static mesh renderer path.
|
subasset places that part through the same static mesh renderer path.
|
||||||
- Brush actors are persisted as `ActorKind::Brush + BrushDesc`; the MVP hydrates additive convex
|
- Brush actors are persisted as `ActorKind::Brush + BrushDesc`; the MVP hydrates additive convex
|
||||||
cube brushes into generated preview meshes while face/edge/CSG editing remains roadmap work.
|
cube brushes into generated preview meshes while face/edge/CSG editing remains roadmap work.
|
||||||
|
- Draw Brush mode (`B` or toolbar pencil) places snapped floor points and commits an additive
|
||||||
|
prism brush with `Enter`; `Esc` or right-click cancels without changing the scene.
|
||||||
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
||||||
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
||||||
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use crate::ui::helpers::{
|
|||||||
use crate::ui::selection_ops::{focus_editor_camera_on_selection, reset_selected_transforms};
|
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::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM};
|
||||||
use crate::ui::UiState;
|
use crate::ui::UiState;
|
||||||
|
use crate::viewport::brush_tool::start_draw_brush_tool;
|
||||||
|
|
||||||
/// Named editor command invokable from the palette and BRP.
|
/// Named editor command invokable from the palette and BRP.
|
||||||
pub trait EditorCommand: Send + Sync {
|
pub trait EditorCommand: Send + Sync {
|
||||||
@ -180,6 +181,7 @@ fn register_builtin_commands(mut registry: ResMut<EditorCommandRegistry>) {
|
|||||||
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));
|
||||||
|
registry.register(Box::new(DrawBrushCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_palette_commands(world: &mut World) {
|
fn run_palette_commands(world: &mut World) {
|
||||||
@ -331,6 +333,27 @@ impl EditorCommand for ResetSelectionTransformCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DrawBrushCommand;
|
||||||
|
|
||||||
|
impl EditorCommand for DrawBrushCommand {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"brush.draw"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> &str {
|
||||||
|
"Draw Brush"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled_reason(&self, world: &World) -> Option<String> {
|
||||||
|
(*world.resource::<State<EditorMode>>().get() != EditorMode::Editing)
|
||||||
|
.then(|| "Enter Edit mode before drawing brushes".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(&self, world: &mut World) {
|
||||||
|
start_draw_brush_tool(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CreatePostProcessVolumeCommand;
|
struct CreatePostProcessVolumeCommand;
|
||||||
|
|
||||||
impl EditorCommand for CreatePostProcessVolumeCommand {
|
impl EditorCommand for CreatePostProcessVolumeCommand {
|
||||||
|
|||||||
@ -29,6 +29,7 @@ pub use scene::scene_io;
|
|||||||
pub use scene::scene_schema;
|
pub use scene::scene_schema;
|
||||||
pub use scene::scene_view;
|
pub use scene::scene_view;
|
||||||
pub use viewport::actor_icons;
|
pub use viewport::actor_icons;
|
||||||
|
pub use viewport::brush_tool;
|
||||||
pub use viewport::camera;
|
pub use viewport::camera;
|
||||||
pub use viewport::gizmos;
|
pub use viewport::gizmos;
|
||||||
pub use viewport::render_view;
|
pub use viewport::render_view;
|
||||||
@ -47,6 +48,7 @@ use transform_gizmo_bevy::prelude::TransformGizmoPlugin;
|
|||||||
|
|
||||||
use actor_icons::ActorIconsPlugin;
|
use actor_icons::ActorIconsPlugin;
|
||||||
use asset_db::AssetDbPlugin;
|
use asset_db::AssetDbPlugin;
|
||||||
|
use brush_tool::BrushToolPlugin;
|
||||||
use camera::EditorCameraPlugin;
|
use camera::EditorCameraPlugin;
|
||||||
use extensibility::ExtensibilityPlugin;
|
use extensibility::ExtensibilityPlugin;
|
||||||
use gizmos::EditorGizmoPlugin;
|
use gizmos::EditorGizmoPlugin;
|
||||||
@ -89,6 +91,7 @@ impl PluginGroup for EditorPluginGroup {
|
|||||||
.add(SceneViewPlugin)
|
.add(SceneViewPlugin)
|
||||||
.add(PlaySessionPlugin)
|
.add(PlaySessionPlugin)
|
||||||
.add(ViewportPlugin)
|
.add(ViewportPlugin)
|
||||||
|
.add(BrushToolPlugin)
|
||||||
.add(EditorCameraPlugin)
|
.add(EditorCameraPlugin)
|
||||||
.add(ActorIconsPlugin)
|
.add(ActorIconsPlugin)
|
||||||
.add(EditorSelectionPlugin)
|
.add(EditorSelectionPlugin)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use crate::history::spawn_with_history;
|
|||||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||||
use crate::state::{EditorMode, PlayPaused, PlayPossession};
|
use crate::state::{EditorMode, PlayPaused, PlayPossession};
|
||||||
use crate::ui::UiState;
|
use crate::ui::UiState;
|
||||||
|
use crate::viewport::brush_tool::start_draw_brush_tool;
|
||||||
use crate::viewport::{editor_spawn_ground_position, snap_translation, ViewportSettings};
|
use crate::viewport::{editor_spawn_ground_position, snap_translation, ViewportSettings};
|
||||||
use shared::PrimitiveShape;
|
use shared::PrimitiveShape;
|
||||||
|
|
||||||
@ -87,6 +88,9 @@ fn left_toolbar(world: &mut World, ui: &mut egui::Ui) {
|
|||||||
let pos = camera_ground_spawn(world);
|
let pos = camera_ground_spawn(world);
|
||||||
spawn_with_history(world, light_snapshot("Point Light", pos));
|
spawn_with_history(world, light_snapshot("Point Light", pos));
|
||||||
}
|
}
|
||||||
|
if icon_button(ui, icons::PENCIL_SIMPLE_LINE, "Draw brush (B)").clicked() {
|
||||||
|
start_draw_brush_tool(world);
|
||||||
|
}
|
||||||
|
|
||||||
toolbar_separator(ui);
|
toolbar_separator(ui);
|
||||||
|
|
||||||
|
|||||||
350
crates/editor/src/viewport/brush_tool.rs
Normal file
350
crates/editor/src/viewport/brush_tool.rs
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
//! Modal floor-polygon brush drawing tool.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::{egui, EguiContexts};
|
||||||
|
use shared::{
|
||||||
|
brush_math::validate_floor_polygon, ActorKind, BrushDesc, EditorVisibility, MaterialDesc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::camera::EditorCamera;
|
||||||
|
use crate::history::{spawn_with_history, EditorEntitySnapshot};
|
||||||
|
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
|
||||||
|
use crate::scene_io::SceneIo;
|
||||||
|
use crate::selection::ViewportClick;
|
||||||
|
use crate::state::scene_tools_active;
|
||||||
|
use crate::ui::{viewport_keyboard_shortcuts_active, UiState};
|
||||||
|
use crate::viewport::{scene_view_ray, snap_translation, ViewportDisplayMode, ViewportSettings};
|
||||||
|
|
||||||
|
const DEFAULT_BRUSH_HEIGHT: f32 = 2.5;
|
||||||
|
const MIN_COMMIT_VERTICES: usize = 3;
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Clone)]
|
||||||
|
pub struct BrushToolState {
|
||||||
|
pub active: bool,
|
||||||
|
pub vertices: Vec<Vec3>,
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrushToolState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
vertices: Vec::new(),
|
||||||
|
height: DEFAULT_BRUSH_HEIGHT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrushToolState {
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.active = true;
|
||||||
|
self.vertices.clear();
|
||||||
|
self.height = DEFAULT_BRUSH_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&mut self) {
|
||||||
|
self.active = false;
|
||||||
|
self.vertices.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BrushToolPlugin;
|
||||||
|
|
||||||
|
impl Plugin for BrushToolPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<BrushToolState>().add_systems(
|
||||||
|
Update,
|
||||||
|
(brush_tool_input, draw_brush_tool_preview).run_if(scene_tools_active),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_draw_brush_tool(world: &mut World) {
|
||||||
|
world.resource_mut::<BrushToolState>().start();
|
||||||
|
world.resource_mut::<SceneIo>().status =
|
||||||
|
"Draw Brush: click floor points, Enter creates brush".to_string();
|
||||||
|
if let Some(mut active_operator) = world.get_resource_mut::<ActiveOperator>() {
|
||||||
|
set_brush_tool_status(
|
||||||
|
&mut active_operator,
|
||||||
|
OperatorPhase::Preview,
|
||||||
|
"Click floor points, Enter creates brush, Esc cancels",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(mut viewport_click) = world.get_resource_mut::<ViewportClick>() {
|
||||||
|
viewport_click.0 = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn brush_tool_input(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut tool: ResMut<BrushToolState>,
|
||||||
|
mut viewport_click: ResMut<ViewportClick>,
|
||||||
|
mut active_operator: ResMut<ActiveOperator>,
|
||||||
|
mut scene_io: ResMut<SceneIo>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
buttons: Res<ButtonInput<MouseButton>>,
|
||||||
|
mut contexts: EguiContexts,
|
||||||
|
ui_state: Res<UiState>,
|
||||||
|
display: Res<ViewportDisplayMode>,
|
||||||
|
settings: Res<ViewportSettings>,
|
||||||
|
cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
|
) -> Result {
|
||||||
|
if display.clean_game_view {
|
||||||
|
if tool.active {
|
||||||
|
tool.cancel();
|
||||||
|
set_brush_tool_status(&mut active_operator, OperatorPhase::Canceled, "Canceled");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = contexts.ctx_mut()?;
|
||||||
|
let egui_keyboard_busy = ctx.wants_keyboard_input();
|
||||||
|
let keyboard_available =
|
||||||
|
viewport_keyboard_shortcuts_active(&ui_state, ctx, &buttons) && !egui_keyboard_busy;
|
||||||
|
|
||||||
|
if keyboard_available && keys.just_pressed(KeyCode::KeyB) {
|
||||||
|
tool.start();
|
||||||
|
scene_io.status = "Draw Brush: click floor points, Enter creates brush".to_string();
|
||||||
|
set_brush_tool_status(
|
||||||
|
&mut active_operator,
|
||||||
|
OperatorPhase::Preview,
|
||||||
|
"Click floor points, Enter creates brush, Esc cancels",
|
||||||
|
);
|
||||||
|
viewport_click.0 = None;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tool.active {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!egui_keyboard_busy && keys.just_pressed(KeyCode::Escape))
|
||||||
|
|| buttons.just_pressed(MouseButton::Right)
|
||||||
|
{
|
||||||
|
tool.cancel();
|
||||||
|
viewport_click.0 = None;
|
||||||
|
scene_io.status = "Draw Brush cancelled".to_string();
|
||||||
|
set_brush_tool_status(&mut active_operator, OperatorPhase::Canceled, "Canceled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !egui_keyboard_busy && keys.just_pressed(KeyCode::Backspace) {
|
||||||
|
tool.vertices.pop();
|
||||||
|
set_brush_tool_status(
|
||||||
|
&mut active_operator,
|
||||||
|
OperatorPhase::Preview,
|
||||||
|
brush_tool_hint(tool.vertices.len()),
|
||||||
|
);
|
||||||
|
viewport_click.0 = None;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pointer_pos) = viewport_click.0.take() {
|
||||||
|
if let Some(mut point) =
|
||||||
|
pointer_floor_position(&cameras, pointer_pos, ui_state.viewport_rect)
|
||||||
|
{
|
||||||
|
point = snap_translation(point, &settings);
|
||||||
|
tool.vertices.push(point);
|
||||||
|
scene_io.status = format!("Draw Brush: {} point(s)", tool.vertices.len());
|
||||||
|
set_brush_tool_status(
|
||||||
|
&mut active_operator,
|
||||||
|
OperatorPhase::Preview,
|
||||||
|
brush_tool_hint(tool.vertices.len()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !egui_keyboard_busy && keys.just_pressed(KeyCode::Enter) {
|
||||||
|
match brush_snapshot_from_points(&tool.vertices, tool.height) {
|
||||||
|
Ok(snapshot) => {
|
||||||
|
commands.queue(move |world: &mut World| {
|
||||||
|
spawn_with_history(world, snapshot);
|
||||||
|
});
|
||||||
|
tool.cancel();
|
||||||
|
scene_io.status = "Brush created".to_string();
|
||||||
|
set_brush_tool_status(
|
||||||
|
&mut active_operator,
|
||||||
|
OperatorPhase::Committed,
|
||||||
|
"Committed brush.draw",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
scene_io.status = format!("Draw Brush failed: {error}");
|
||||||
|
set_brush_tool_status(&mut active_operator, OperatorPhase::Blocked, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_brush_tool_preview(
|
||||||
|
tool: Res<BrushToolState>,
|
||||||
|
ui_state: Res<UiState>,
|
||||||
|
cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
) {
|
||||||
|
if !tool.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut points = tool.vertices.clone();
|
||||||
|
if let Some(pointer_pos) = ui_state.viewport_pointer_pos {
|
||||||
|
if let Some(point) = pointer_floor_position(&cameras, pointer_pos, ui_state.viewport_rect) {
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if points.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let color = Color::srgb(0.2, 0.72, 1.0);
|
||||||
|
for point in &points {
|
||||||
|
gizmos.sphere(*point + Vec3::Y * 0.02, 0.08, color);
|
||||||
|
gizmos.line(*point, *point + Vec3::Y * tool.height, color);
|
||||||
|
}
|
||||||
|
for window in points.windows(2) {
|
||||||
|
gizmos.line(window[0], window[1], color);
|
||||||
|
gizmos.line(
|
||||||
|
window[0] + Vec3::Y * tool.height,
|
||||||
|
window[1] + Vec3::Y * tool.height,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if points.len() >= MIN_COMMIT_VERTICES {
|
||||||
|
let first = points[0];
|
||||||
|
let last = *points.last().expect("points checked non-empty");
|
||||||
|
gizmos.line(last, first, color);
|
||||||
|
gizmos.line(
|
||||||
|
last + Vec3::Y * tool.height,
|
||||||
|
first + Vec3::Y * tool.height,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pointer_floor_position(
|
||||||
|
cameras: &Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
scene_rect: egui::Rect,
|
||||||
|
) -> Option<Vec3> {
|
||||||
|
let (camera, transform) = cameras
|
||||||
|
.iter()
|
||||||
|
.find(|(camera, _)| camera.is_active)
|
||||||
|
.or_else(|| cameras.iter().next())?;
|
||||||
|
let ray = scene_view_ray(camera, transform, pointer_pos, scene_rect)?;
|
||||||
|
let direction = ray.direction.as_vec3();
|
||||||
|
if direction.y.abs() < 0.0001 {
|
||||||
|
return Some(ray.origin);
|
||||||
|
}
|
||||||
|
let t = -ray.origin.y / direction.y;
|
||||||
|
Some(ray.origin + direction * t.max(0.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn brush_snapshot_from_points(
|
||||||
|
points: &[Vec3],
|
||||||
|
height: f32,
|
||||||
|
) -> Result<EditorEntitySnapshot, String> {
|
||||||
|
validate_floor_polygon(points).map_err(|error| format!("{error:?}"))?;
|
||||||
|
let center = polygon_center(points);
|
||||||
|
let local_points: Vec<Vec3> = points.iter().map(|point| *point - center).collect();
|
||||||
|
let brush = BrushDesc::extruded_prism(&local_points, height)
|
||||||
|
.ok_or_else(|| "invalid brush prism".to_string())?;
|
||||||
|
Ok(EditorEntitySnapshot {
|
||||||
|
actor_id: None,
|
||||||
|
actor_kind: ActorKind::Brush,
|
||||||
|
actor_name: None,
|
||||||
|
name: Some("Brush".to_string()),
|
||||||
|
transform: Transform::from_translation(center),
|
||||||
|
primitive: None,
|
||||||
|
brush: Some(brush),
|
||||||
|
static_mesh_renderer: None,
|
||||||
|
material: Some(MaterialDesc::default()),
|
||||||
|
material_override: None,
|
||||||
|
rigid_body: None,
|
||||||
|
collider: None,
|
||||||
|
physics: None,
|
||||||
|
light: None,
|
||||||
|
player_spawn: false,
|
||||||
|
model: None,
|
||||||
|
prefab: None,
|
||||||
|
prefab_instance: None,
|
||||||
|
weapon_spawn: None,
|
||||||
|
trigger_volume: None,
|
||||||
|
post_process_volume: None,
|
||||||
|
team_spawn: None,
|
||||||
|
objective: None,
|
||||||
|
hierarchy_sibling_index: 0,
|
||||||
|
editor_visibility: EditorVisibility::default(),
|
||||||
|
children: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polygon_center(points: &[Vec3]) -> Vec3 {
|
||||||
|
let sum = points
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.fold(Vec3::ZERO, |sum, point| sum + point);
|
||||||
|
sum / points.len() as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn brush_tool_hint(vertex_count: usize) -> String {
|
||||||
|
match vertex_count {
|
||||||
|
0 => "Click first floor point".to_string(),
|
||||||
|
1 => "Click second floor point".to_string(),
|
||||||
|
2 => "Click third floor point".to_string(),
|
||||||
|
count => format!("{count} points, Enter creates brush"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_brush_tool_status(
|
||||||
|
active_operator: &mut ActiveOperator,
|
||||||
|
phase: OperatorPhase,
|
||||||
|
hint: impl Into<String>,
|
||||||
|
) {
|
||||||
|
active_operator.status = Some(OperatorStatus {
|
||||||
|
id: "brush.draw".to_string(),
|
||||||
|
label: "Draw Brush".to_string(),
|
||||||
|
phase,
|
||||||
|
hint: hint.into(),
|
||||||
|
warnings: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brush_snapshot_centers_polygon_points() {
|
||||||
|
let snapshot = brush_snapshot_from_points(
|
||||||
|
&[
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
Vec3::new(2.0, 0.0, 0.0),
|
||||||
|
Vec3::new(2.0, 0.0, 2.0),
|
||||||
|
Vec3::new(0.0, 0.0, 2.0),
|
||||||
|
],
|
||||||
|
3.0,
|
||||||
|
)
|
||||||
|
.expect("valid brush");
|
||||||
|
|
||||||
|
assert_eq!(snapshot.actor_kind, ActorKind::Brush);
|
||||||
|
assert_eq!(snapshot.transform.translation, Vec3::new(1.0, 0.0, 1.0));
|
||||||
|
assert!(snapshot.brush.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brush_snapshot_rejects_non_convex_polygon() {
|
||||||
|
let result = brush_snapshot_from_points(
|
||||||
|
&[
|
||||||
|
Vec3::new(-1.0, 0.0, -1.0),
|
||||||
|
Vec3::new(1.0, 0.0, -1.0),
|
||||||
|
Vec3::new(0.0, 0.0, 0.0),
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(-1.0, 0.0, 1.0),
|
||||||
|
],
|
||||||
|
2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
//! Viewport chrome, editor camera, selection, gizmos, and render views.
|
//! Viewport chrome, editor camera, selection, gizmos, and render views.
|
||||||
|
|
||||||
pub mod actor_icons;
|
pub mod actor_icons;
|
||||||
|
pub mod brush_tool;
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
pub mod gizmos;
|
pub mod gizmos;
|
||||||
mod panel;
|
mod panel;
|
||||||
@ -12,5 +13,6 @@ pub mod viewport_mode;
|
|||||||
pub mod visualizers;
|
pub mod visualizers;
|
||||||
|
|
||||||
pub use actor_icons::ActorIconsPlugin;
|
pub use actor_icons::ActorIconsPlugin;
|
||||||
|
pub use brush_tool::{BrushToolPlugin, BrushToolState};
|
||||||
pub use panel::*;
|
pub use panel::*;
|
||||||
pub use viewport_mode::EditorViewportMode;
|
pub use viewport_mode::EditorViewportMode;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ use crate::ui::hierarchy_ops::is_entity_locked;
|
|||||||
use crate::ui::hierarchy_state::HierarchyPanelState;
|
use crate::ui::hierarchy_state::HierarchyPanelState;
|
||||||
use crate::ui::UiState;
|
use crate::ui::UiState;
|
||||||
use crate::viewport::actor_icons::ActorIconProxy;
|
use crate::viewport::actor_icons::ActorIconProxy;
|
||||||
|
use crate::viewport::brush_tool::BrushToolState;
|
||||||
use crate::viewport::ViewportDisplayMode;
|
use crate::viewport::ViewportDisplayMode;
|
||||||
use crate::visualizers::EditorVisualizerProxy;
|
use crate::visualizers::EditorVisualizerProxy;
|
||||||
use bevy_egui::egui;
|
use bevy_egui::egui;
|
||||||
@ -102,6 +103,7 @@ fn handle_pick_events(
|
|||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
buttons: Res<ButtonInput<MouseButton>>,
|
buttons: Res<ButtonInput<MouseButton>>,
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
|
brush_tool: Res<BrushToolState>,
|
||||||
gizmo_targets: Query<&GizmoTarget>,
|
gizmo_targets: Query<&GizmoTarget>,
|
||||||
hierarchy: Option<Res<HierarchyPanelState>>,
|
hierarchy: Option<Res<HierarchyPanelState>>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
@ -122,6 +124,10 @@ fn handle_pick_events(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if brush_tool.active {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if gizmo_targets
|
if gizmo_targets
|
||||||
.iter()
|
.iter()
|
||||||
.any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
|
.any(|g| GizmoTarget::is_focused(g) || GizmoTarget::is_active(g))
|
||||||
|
|||||||
131
crates/shared/src/brush_math.rs
Normal file
131
crates/shared/src/brush_math.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
//! Brush geometry validation helpers shared by editor tools and hydration.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
const EPSILON: f32 = 0.0001;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BrushPolygonError {
|
||||||
|
TooFewVertices,
|
||||||
|
NonFiniteVertex,
|
||||||
|
DuplicateVertex,
|
||||||
|
ZeroArea,
|
||||||
|
SelfIntersecting,
|
||||||
|
NonConvex,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signed_area_xz(vertices: &[Vec3]) -> f32 {
|
||||||
|
if vertices.len() < 3 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
vertices
|
||||||
|
.iter()
|
||||||
|
.zip(vertices.iter().cycle().skip(1))
|
||||||
|
.take(vertices.len())
|
||||||
|
.map(|(a, b)| a.x * b.z - b.x * a.z)
|
||||||
|
.sum::<f32>()
|
||||||
|
* 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_floor_polygon(vertices: &[Vec3]) -> Result<(), BrushPolygonError> {
|
||||||
|
if vertices.len() < 3 {
|
||||||
|
return Err(BrushPolygonError::TooFewVertices);
|
||||||
|
}
|
||||||
|
if !vertices.iter().all(|vertex| vertex.is_finite()) {
|
||||||
|
return Err(BrushPolygonError::NonFiniteVertex);
|
||||||
|
}
|
||||||
|
for i in 0..vertices.len() {
|
||||||
|
for j in (i + 1)..vertices.len() {
|
||||||
|
if vertices[i].xz().distance_squared(vertices[j].xz()) <= EPSILON * EPSILON {
|
||||||
|
return Err(BrushPolygonError::DuplicateVertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let area = signed_area_xz(vertices);
|
||||||
|
if self_intersects_xz(vertices) {
|
||||||
|
return Err(BrushPolygonError::SelfIntersecting);
|
||||||
|
}
|
||||||
|
if area.abs() <= EPSILON {
|
||||||
|
return Err(BrushPolygonError::ZeroArea);
|
||||||
|
}
|
||||||
|
if !is_convex_xz(vertices) {
|
||||||
|
return Err(BrushPolygonError::NonConvex);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn self_intersects_xz(vertices: &[Vec3]) -> bool {
|
||||||
|
for a in 0..vertices.len() {
|
||||||
|
let b = (a + 1) % vertices.len();
|
||||||
|
for c in (a + 1)..vertices.len() {
|
||||||
|
let d = (c + 1) % vertices.len();
|
||||||
|
if a == c || a == d || b == c || b == d {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if segments_intersect_xz(vertices[a], vertices[b], vertices[c], vertices[d]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segments_intersect_xz(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> bool {
|
||||||
|
let ab_c = orient_xz(a, b, c);
|
||||||
|
let ab_d = orient_xz(a, b, d);
|
||||||
|
let cd_a = orient_xz(c, d, a);
|
||||||
|
let cd_b = orient_xz(c, d, b);
|
||||||
|
ab_c.signum() != ab_d.signum() && cd_a.signum() != cd_b.signum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_convex_xz(vertices: &[Vec3]) -> bool {
|
||||||
|
let winding = signed_area_xz(vertices).signum();
|
||||||
|
vertices.iter().enumerate().all(|(index, current)| {
|
||||||
|
let prev = vertices[(index + vertices.len() - 1) % vertices.len()];
|
||||||
|
let next = vertices[(index + 1) % vertices.len()];
|
||||||
|
let turn = orient_xz(prev, *current, next);
|
||||||
|
turn.abs() <= EPSILON || turn.signum() == winding
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orient_xz(a: Vec3, b: Vec3, c: Vec3) -> f32 {
|
||||||
|
let ab = b.xz() - a.xz();
|
||||||
|
let ac = c.xz() - a.xz();
|
||||||
|
ab.x * ac.y - ab.y * ac.x
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn p(x: f32, z: f32) -> Vec3 {
|
||||||
|
Vec3::new(x, 0.0, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_convex_floor_polygon() {
|
||||||
|
assert!(validate_floor_polygon(&[p(-1.0, -1.0), p(1.0, -1.0), p(1.0, 1.0)]).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_self_intersecting_polygon() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_floor_polygon(&[p(-1.0, -1.0), p(1.0, 1.0), p(1.0, -1.0), p(-1.0, 1.0),]),
|
||||||
|
Err(BrushPolygonError::SelfIntersecting)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_convex_polygon() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_floor_polygon(&[
|
||||||
|
p(-1.0, -1.0),
|
||||||
|
p(1.0, -1.0),
|
||||||
|
p(0.0, 0.0),
|
||||||
|
p(1.0, 1.0),
|
||||||
|
p(-1.0, 1.0),
|
||||||
|
]),
|
||||||
|
Err(BrushPolygonError::NonConvex)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -426,6 +426,49 @@ impl BrushDesc {
|
|||||||
receive_shadows: true,
|
receive_shadows: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extruded_prism(base_vertices: &[Vec3], height: f32) -> Option<Self> {
|
||||||
|
if crate::brush_math::validate_floor_polygon(base_vertices).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let height = height.max(0.001);
|
||||||
|
let mut base = base_vertices.to_vec();
|
||||||
|
if crate::brush_math::signed_area_xz(&base) < 0.0 {
|
||||||
|
base.reverse();
|
||||||
|
}
|
||||||
|
let top: Vec<Vec3> = base
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| *vertex + Vec3::Y * height)
|
||||||
|
.collect();
|
||||||
|
let mut bottom = base.clone();
|
||||||
|
bottom.reverse();
|
||||||
|
|
||||||
|
let mut faces = Vec::with_capacity(base.len() + 2);
|
||||||
|
faces.push(brush_face("face:+y", Vec3::Y, top));
|
||||||
|
faces.push(brush_face("face:-y", Vec3::NEG_Y, bottom));
|
||||||
|
for index in 0..base.len() {
|
||||||
|
let next = (index + 1) % base.len();
|
||||||
|
let a = base[index];
|
||||||
|
let b = base[next];
|
||||||
|
let edge = b - a;
|
||||||
|
let normal = Vec3::new(edge.z, 0.0, -edge.x).normalize_or_zero();
|
||||||
|
if normal.length_squared() <= f32::EPSILON {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
faces.push(brush_face(
|
||||||
|
&format!("face:side:{index}"),
|
||||||
|
normal,
|
||||||
|
vec![a, a + Vec3::Y * height, b + Vec3::Y * height, b],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
kind: BrushKind::Additive,
|
||||||
|
faces,
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BrushDesc {
|
impl Default for BrushDesc {
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
//! from them by hydration systems in both the game and editor.
|
//! from them by hydration systems in both the game and editor.
|
||||||
|
|
||||||
mod actor;
|
mod actor;
|
||||||
|
pub mod brush_math;
|
||||||
mod components;
|
mod components;
|
||||||
mod hydration;
|
mod hydration;
|
||||||
mod material_asset;
|
mod material_asset;
|
||||||
|
|||||||
@ -44,6 +44,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`.
|
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`.
|
||||||
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
|
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
|
||||||
- **Brush authoring** — `ActorKind::Brush + BrushDesc` stores persisted convex blockout faces and hydrates active brushes into generated mesh children. See [brushes.md](brushes.md).
|
- **Brush authoring** — `ActorKind::Brush + BrushDesc` stores persisted convex blockout faces and hydrates active brushes into generated mesh children. See [brushes.md](brushes.md).
|
||||||
|
- **Draw Brush** — `B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter creates an additive prism brush through history, and Esc/right-click cancels.
|
||||||
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
||||||
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits live on the actor `MaterialDesc`; static mesh slot material refs are source/default selectors, and content-browser asset inspectors are the path for editing shared material assets.
|
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits live on the actor `MaterialDesc`; static mesh slot material refs are source/default selectors, and content-browser asset inspectors are the path for editing shared material assets.
|
||||||
- **Prefab overrides** — `PrefabInstance.overrides_ron` stores transform/material/child-visibility overrides; inspector collapsible groups with **Apply** / **Revert** (replaces transform-only revert v1).
|
- **Prefab overrides** — `PrefabInstance.overrides_ron` stores transform/material/child-visibility overrides; inspector collapsible groups with **Apply** / **Revert** (replaces transform-only revert v1).
|
||||||
@ -53,7 +54,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.
|
- **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/source-material selectors, visibility, and shadow flags. Actor material authoring lives in the `Authoring Material` component, whose texture refs use Asset Browser picker/drop controls. Empty source 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 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/source-material selectors, visibility, and shadow flags. Actor material authoring lives in the `Authoring Material` component, whose texture refs use Asset Browser picker/drop controls. Empty source 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`): focuses the filter on open, selects the first filtered command, supports arrow-key selection and Enter to run. Commands include `scene.reset_lighting`, `brush.draw`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, `selection.reset_transform`, and play commands.
|
||||||
- **Editor input routing** gives egui text fields first claim on keyboard input. Viewport shortcut keys require viewport pointer focus and are suspended while RMB/MMB camera navigation is active.
|
- **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.
|
||||||
|
|||||||
@ -9,6 +9,7 @@ Brushes are persisted blockout geometry stored as `ActorKind::Brush + BrushDesc`
|
|||||||
- Hydration builds generated mesh children from active brush components and strips those children before scene save.
|
- Hydration builds generated mesh children from active brush components and strips those children before scene save.
|
||||||
- Brush actors validate separately from primitives and imported/static mesh actors.
|
- Brush actors validate separately from primitives and imported/static mesh actors.
|
||||||
- The Actor Inspector Add Component shelf can add a **Brush** component. The Brush card exposes kind, face count, shadow flags, and cube reset.
|
- The Actor Inspector Add Component shelf can add a **Brush** component. The Brush card exposes kind, face count, shadow flags, and cube reset.
|
||||||
|
- Draw Brush mode (`B`, toolbar pencil, or command `brush.draw`) creates additive prism brushes from snapped floor points. LMB places points, Backspace removes the last point, Enter commits through undo history, and Esc/right-click cancels without scene mutation.
|
||||||
|
|
||||||
## Current Limits
|
## Current Limits
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user