Complete M0 editor asset workflow
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
This commit is contained in:
parent
d77cc6a40e
commit
cc6acb188d
@ -26,7 +26,7 @@ avian3d = { version = "0.6", default-features = false, features = [
|
||||
"collider-from-mesh",
|
||||
"serialize",
|
||||
] }
|
||||
bevy = { version = "0.18", features = ["serialize"] }
|
||||
bevy = { version = "0.18", features = ["serialize", "jpeg"] }
|
||||
bevy_core_pipeline = "0.18"
|
||||
bevy_solari = "0.18"
|
||||
bevy_ufbx = "0.18"
|
||||
|
||||
@ -294,7 +294,7 @@ crates/
|
||||
- [x] Prefab instance overrides v2 (transform/material/child visibility apply-revert groups)
|
||||
- [x] Advanced rendering: `GiMode`, post-process volumes, Rendering panel, requested/effective render stack, Solari integration, emissive materials, post FX assets ([ADR 0013](docs/adr/0013-rendering-tiers-and-post-process-volumes.md), [ADR 0016](docs/adr/0016-unified-rendering-contract.md), [rendering guide](docs/editor/rendering.md))
|
||||
- [x] Static mesh renderer component, generated normalized mesh artifacts, source/one-actor hierarchy placement, and inspector renderer slots ([ADR 0017](docs/adr/0017-normalized-static-mesh-assets.md))
|
||||
- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware material data, and per-actor material overrides ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md))
|
||||
- [x] Componentized actor inspector, unified component cards, thumbnail static mesh slots, imported asset Browse/Locate/Clear refs with inherited source defaults, collider split, shader-aware actor material data, and texture picker/drop refs ([ADR 0018](docs/adr/0018-componentized-actor-inspector-and-materials.md))
|
||||
|
||||
## Notes / Future Work
|
||||
|
||||
@ -311,7 +311,7 @@ crates/
|
||||
edited from the browser, and delete actions move files to `assets/.trash/`. Skeletal animation
|
||||
playback is not supported by the static mesh path yet.
|
||||
- Prefab instances use stable asset IDs (`PrefabInstance` + registry) with serialized `PrefabOverrides` (transform, material, per-child visibility by name). Inspector **Apply/Revert** per field group; **Unpack** removes the instance link.
|
||||
- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. The Asset Browser material details editor can apply shader schemas, edit typed parameters/textures, and regenerate sphere thumbnails. Actor inspector edits create per-actor `MaterialOverride` data for static mesh slots instead of mutating shared material assets.
|
||||
- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. The Asset Browser material details editor can apply shader schemas, edit typed parameters/textures, and regenerate sphere thumbnails. Actor inspector material edits live on the actor `MaterialDesc`; static mesh source-material refs remain imported defaults and shared material assets are edited from the Asset Browser.
|
||||
- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path.
|
||||
- The authoring/hydration layer is intentionally small so richer asset workflows (terrain,
|
||||
material graphs, lighting profiles) can be added without changing the scene format foundation.
|
||||
|
||||
@ -4,6 +4,7 @@ pub mod asset_db;
|
||||
mod catalog;
|
||||
mod import;
|
||||
pub mod materials;
|
||||
pub mod operators;
|
||||
pub mod prefab_overrides;
|
||||
pub mod static_mesh;
|
||||
pub mod thumbnails;
|
||||
|
||||
93
crates/editor/src/assets/operators.rs
Normal file
93
crates/editor/src/assets/operators.rs
Normal file
@ -0,0 +1,93 @@
|
||||
//! Operator wrappers for asset placement and assignment workflows.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
||||
|
||||
use crate::operators::{run_immediate_operator, OperatorAvailability};
|
||||
use crate::ui::helpers::{
|
||||
apply_material_asset_to_selection, apply_texture_to_selection, selected_level_entities,
|
||||
};
|
||||
|
||||
use super::{spawn_asset_at, spawn_subasset_at, AssetSelection, EditorAsset};
|
||||
|
||||
fn selection_availability(world: &World, selected: &SelectedEntities) -> OperatorAvailability {
|
||||
if selected_level_entities(world, selected).is_empty() {
|
||||
OperatorAvailability::Disabled("Select at least one authored actor".to_string())
|
||||
} else {
|
||||
OperatorAvailability::Ready
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_asset_operator(world: &mut World, asset: EditorAsset, translation: Vec3) -> bool {
|
||||
let label = format!("Place {}", asset.label);
|
||||
run_immediate_operator(
|
||||
world,
|
||||
"assets.place",
|
||||
&label,
|
||||
OperatorAvailability::Ready,
|
||||
move |world| {
|
||||
spawn_asset_at(world, &asset, translation)
|
||||
.map(|_| ())
|
||||
.ok_or_else(|| format!("Asset cannot be placed: {}", asset.label))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn place_subasset_operator(
|
||||
world: &mut World,
|
||||
selection: AssetSelection,
|
||||
translation: Vec3,
|
||||
) -> bool {
|
||||
let label = format!("Place {}", selection.display_label());
|
||||
run_immediate_operator(
|
||||
world,
|
||||
"assets.place_subasset",
|
||||
&label,
|
||||
OperatorAvailability::Ready,
|
||||
move |world| {
|
||||
spawn_subasset_at(world, &selection, translation)
|
||||
.map(|_| ())
|
||||
.ok_or_else(|| format!("Sub-asset cannot be placed: {}", selection.display_label()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_texture_operator(
|
||||
world: &mut World,
|
||||
asset: EditorAsset,
|
||||
selected: &SelectedEntities,
|
||||
) -> bool {
|
||||
let label = format!("Apply Texture {}", asset.label);
|
||||
let availability = selection_availability(world, selected);
|
||||
let selected = selected;
|
||||
run_immediate_operator(
|
||||
world,
|
||||
"assets.apply_texture",
|
||||
&label,
|
||||
availability,
|
||||
move |world| {
|
||||
apply_texture_to_selection(world, &asset, &selected);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_material_operator(
|
||||
world: &mut World,
|
||||
asset: EditorAsset,
|
||||
selected: &SelectedEntities,
|
||||
) -> bool {
|
||||
let label = format!("Apply Material {}", asset.label);
|
||||
let availability = selection_availability(world, selected);
|
||||
let selected = selected;
|
||||
run_immediate_operator(
|
||||
world,
|
||||
"assets.apply_material",
|
||||
&label,
|
||||
availability,
|
||||
move |world| {
|
||||
apply_material_asset_to_selection(world, &asset, &selected);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -16,8 +16,8 @@ use shared::{
|
||||
};
|
||||
|
||||
use crate::assets::{
|
||||
spawn_asset_at, spawn_subasset_at, AssetSelection, AssetSubAssetKind, EditorAsset,
|
||||
EditorAssetKind, EditorAssets, ASSETS_ROOT, BUILTINS_FOLDER,
|
||||
AssetSelection, AssetSubAssetKind, EditorAsset, EditorAssetKind, EditorAssets, ASSETS_ROOT,
|
||||
BUILTINS_FOLDER,
|
||||
};
|
||||
use crate::scene_io::{SceneIo, SceneIoRequest};
|
||||
|
||||
@ -29,6 +29,9 @@ use crate::asset_db::{
|
||||
find_asset_by_path, find_asset_mut_by_path, save_registry, update_import_settings,
|
||||
ImportSettings, MaterialImportPolicy, ModelHierarchyMode, ModelPlacementMode,
|
||||
};
|
||||
use crate::assets::operators::{
|
||||
apply_material_operator, apply_texture_operator, place_asset_operator, place_subasset_operator,
|
||||
};
|
||||
use crate::assets::static_mesh::{
|
||||
load_static_mesh_manifest, material_id_from_label, part_id_from_label,
|
||||
refresh_static_mesh_artifact,
|
||||
@ -37,9 +40,7 @@ use crate::assets::{
|
||||
asset_cache_key, draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon,
|
||||
prefetch_folder_thumbnails, AssetThumbnailCache, ThumbnailCacheSnapshot, ThumbnailStudio,
|
||||
};
|
||||
use crate::ui::helpers::{
|
||||
apply_material_asset_to_selection, apply_texture_to_selection, asset_label,
|
||||
};
|
||||
use crate::ui::helpers::asset_label;
|
||||
use crate::ui::theme::{panel_heading, ACCENT, BORDER, ELEVATED_BG, TEXT, TEXT_DIM, WIDGET_BG};
|
||||
use crate::ui::widgets::{icon_button_small, tool_button};
|
||||
|
||||
@ -724,14 +725,14 @@ fn selection_footer(world: &mut World, ui: &mut egui::Ui, selected_entities: &Se
|
||||
AssetSubAssetKind::Mesh => {
|
||||
if ui.button("Place At Origin").clicked() {
|
||||
if let Some(selection) = selection.as_ref() {
|
||||
spawn_subasset_at(world, selection, Vec3::ZERO);
|
||||
place_subasset_operator(world, selection.clone(), Vec3::ZERO);
|
||||
}
|
||||
}
|
||||
}
|
||||
AssetSubAssetKind::Texture => {
|
||||
if ui.button("Apply Texture To Selection").clicked() {
|
||||
if let Some(asset) = texture_asset_from_subasset_selection(world, &selection) {
|
||||
apply_texture_to_selection(world, &asset, selected_entities);
|
||||
apply_texture_operator(world, asset, selected_entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -745,7 +746,7 @@ fn selection_footer(world: &mut World, ui: &mut egui::Ui, selected_entities: &Se
|
||||
if matches!(asset.kind, EditorAssetKind::Texture)
|
||||
&& ui.button("Apply Texture To Selection").clicked()
|
||||
{
|
||||
apply_texture_to_selection(world, &asset, selected_entities);
|
||||
apply_texture_operator(world, asset.clone(), selected_entities);
|
||||
}
|
||||
if matches!(
|
||||
asset.kind,
|
||||
@ -756,7 +757,7 @@ fn selection_footer(world: &mut World, ui: &mut egui::Ui, selected_entities: &Se
|
||||
| EditorAssetKind::Level
|
||||
) && ui.button("Place At Origin").clicked()
|
||||
{
|
||||
spawn_asset_at(world, &asset, Vec3::ZERO);
|
||||
place_asset_operator(world, asset.clone(), Vec3::ZERO);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -1796,7 +1797,7 @@ fn subasset_details_panel(
|
||||
match embedded.kind {
|
||||
AssetSubAssetKind::Mesh => {
|
||||
if ui.button("Place At Origin").clicked() {
|
||||
spawn_subasset_at(world, selection, Vec3::ZERO);
|
||||
place_subasset_operator(world, selection.clone(), Vec3::ZERO);
|
||||
}
|
||||
}
|
||||
AssetSubAssetKind::Texture => {
|
||||
@ -1804,7 +1805,7 @@ fn subasset_details_panel(
|
||||
if let Some(asset) =
|
||||
texture_asset_from_subasset_selection(world, &Some(selection.clone()))
|
||||
{
|
||||
apply_texture_to_selection(world, &asset, selected_entities);
|
||||
apply_texture_operator(world, asset, selected_entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1832,11 +1833,11 @@ fn asset_action_buttons(
|
||||
) {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if matches!(asset.kind, EditorAssetKind::Texture) && ui.button("Apply Texture").clicked() {
|
||||
apply_texture_to_selection(world, asset, selected_entities);
|
||||
apply_texture_operator(world, asset.clone(), selected_entities);
|
||||
}
|
||||
if matches!(asset.kind, EditorAssetKind::Material) && ui.button("Apply Material").clicked()
|
||||
{
|
||||
apply_material_asset_to_selection(world, asset, selected_entities);
|
||||
apply_material_operator(world, asset.clone(), selected_entities);
|
||||
}
|
||||
if matches!(
|
||||
asset.kind,
|
||||
@ -1847,7 +1848,7 @@ fn asset_action_buttons(
|
||||
| EditorAssetKind::Level
|
||||
) && ui.button("Place At Origin").clicked()
|
||||
{
|
||||
spawn_asset_at(world, asset, Vec3::ZERO);
|
||||
place_asset_operator(world, asset.clone(), Vec3::ZERO);
|
||||
}
|
||||
if matches!(asset.kind, EditorAssetKind::Model)
|
||||
&& has_embedded_assets(world, asset)
|
||||
@ -2808,7 +2809,7 @@ fn asset_context_menu(
|
||||
| EditorAssetKind::Level
|
||||
) && ui.button("Place at Origin").clicked()
|
||||
{
|
||||
spawn_asset_at(world, asset, Vec3::ZERO);
|
||||
place_asset_operator(world, asset.clone(), Vec3::ZERO);
|
||||
ui.close();
|
||||
}
|
||||
if matches!(asset.kind, EditorAssetKind::Model) && has_embedded_assets(world, asset) {
|
||||
@ -2835,13 +2836,13 @@ fn asset_context_menu(
|
||||
if matches!(asset.kind, EditorAssetKind::Texture)
|
||||
&& ui.button("Apply as Base Color Texture").clicked()
|
||||
{
|
||||
apply_texture_to_selection(world, asset, selected_entities);
|
||||
apply_texture_operator(world, asset.clone(), selected_entities);
|
||||
ui.close();
|
||||
}
|
||||
if matches!(asset.kind, EditorAssetKind::Material)
|
||||
&& ui.button("Apply Material To Selection").clicked()
|
||||
{
|
||||
apply_material_asset_to_selection(world, asset, selected_entities);
|
||||
apply_material_operator(world, asset.clone(), selected_entities);
|
||||
ui.close();
|
||||
}
|
||||
if matches!(asset.kind, EditorAssetKind::Model) && ui.button("Reimport").clicked() {
|
||||
@ -2880,7 +2881,7 @@ fn subasset_context_menu(
|
||||
match embedded.kind {
|
||||
AssetSubAssetKind::Mesh => {
|
||||
if ui.button("Place at Origin").clicked() {
|
||||
spawn_subasset_at(world, &embedded.selection, Vec3::ZERO);
|
||||
place_subasset_operator(world, embedded.selection.clone(), Vec3::ZERO);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
@ -2889,7 +2890,7 @@ fn subasset_context_menu(
|
||||
if let Some(asset) =
|
||||
texture_asset_from_subasset_selection(world, &Some(embedded.selection.clone()))
|
||||
{
|
||||
apply_texture_to_selection(world, &asset, selected_entities);
|
||||
apply_texture_operator(world, asset, selected_entities);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@ -9,23 +9,23 @@ use egui_phosphor_icons::{icons, Icon};
|
||||
use shared::{
|
||||
inspector_component_active, AuthoringLightKind, AuthoringRigidBody, ColliderDesc,
|
||||
ColliderShapeDesc, ColorDesc, ComponentInstanceId, EditorAssetRef, InspectorOrder, LevelObject,
|
||||
LightDesc, MaterialDesc, MaterialOverride, MaterialParameter, MaterialParameterValue,
|
||||
MaterialShaderKind, MaterialSlotOverride, ObjectiveMarker, PhysicsBody, PlayerSpawn,
|
||||
PostProcessVolumeDesc, PrefabInstance, Primitive, PrimitiveShape, ProjectSun, RigidBodyDesc,
|
||||
StaticMeshRenderer, StaticMeshRendererEntry, TeamSpawn, TriggerVolume, WeaponSpawn,
|
||||
AUTHORING_DIRECTIONAL_LUX_MAX, AUTHORING_POINT_SPOT_LUMENS_MAX, COMPONENT_COLLIDER_DESC,
|
||||
COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC, COMPONENT_OBJECTIVE_MARKER,
|
||||
COMPONENT_PHYSICS_BODY, COMPONENT_PLAYER_SPAWN, COMPONENT_POST_PROCESS_VOLUME,
|
||||
COMPONENT_PREFAB_INSTANCE, COMPONENT_PRIMITIVE, COMPONENT_PROJECT_SUN,
|
||||
COMPONENT_RIGID_BODY_DESC, COMPONENT_STATIC_MESH_RENDERER, COMPONENT_TEAM_SPAWN,
|
||||
COMPONENT_TRIGGER_VOLUME, COMPONENT_WEAPON_SPAWN,
|
||||
LightDesc, MaterialDesc, MaterialParameter, MaterialParameterValue, MaterialShaderKind,
|
||||
ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, Primitive,
|
||||
PrimitiveShape, ProjectSun, RigidBodyDesc, StaticMeshRenderer, StaticMeshRendererEntry,
|
||||
TeamSpawn, TriggerVolume, WeaponSpawn, AUTHORING_DIRECTIONAL_LUX_MAX,
|
||||
AUTHORING_POINT_SPOT_LUMENS_MAX, COMPONENT_COLLIDER_DESC, COMPONENT_LIGHT_DESC,
|
||||
COMPONENT_MATERIAL_DESC, COMPONENT_OBJECTIVE_MARKER, COMPONENT_PHYSICS_BODY,
|
||||
COMPONENT_PLAYER_SPAWN, COMPONENT_POST_PROCESS_VOLUME, COMPONENT_PREFAB_INSTANCE,
|
||||
COMPONENT_PRIMITIVE, COMPONENT_PROJECT_SUN, COMPONENT_RIGID_BODY_DESC,
|
||||
COMPONENT_STATIC_MESH_RENDERER, COMPONENT_TEAM_SPAWN, COMPONENT_TRIGGER_VOLUME,
|
||||
COMPONENT_WEAPON_SPAWN,
|
||||
};
|
||||
|
||||
use crate::history::{
|
||||
material_eq, set_collider_with_history, set_inspector_order_with_history,
|
||||
set_light_with_history, set_material_override_with_history, set_material_with_history,
|
||||
set_physics_with_history, set_post_process_volume_with_history, set_primitive_with_history,
|
||||
set_rigid_body_with_history, set_static_mesh_renderer_with_history, EditorEntitySnapshot,
|
||||
set_light_with_history, set_material_with_history, set_physics_with_history,
|
||||
set_post_process_volume_with_history, set_primitive_with_history, set_rigid_body_with_history,
|
||||
set_static_mesh_renderer_with_history, EditorEntitySnapshot,
|
||||
};
|
||||
use crate::selection::SelectedEntity;
|
||||
use crate::ui::theme::{
|
||||
@ -46,8 +46,8 @@ use crate::assets::static_mesh::{
|
||||
};
|
||||
use crate::assets::thumbnails::ThumbnailStudio;
|
||||
use crate::assets::{
|
||||
asset_cache_key, AssetSelection, AssetThumbnailCache, EditorAsset, EditorAssetKind,
|
||||
EditorAssets,
|
||||
asset_cache_key, AssetSelection, AssetSubAssetKind, AssetThumbnailCache, EditorAsset,
|
||||
EditorAssetKind, EditorAssets,
|
||||
};
|
||||
|
||||
const COMPACT_INSPECTOR_WIDTH: f32 = 360.0;
|
||||
@ -98,6 +98,15 @@ struct AssetSelectorResponse {
|
||||
locate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TextureAssetCandidate {
|
||||
label: String,
|
||||
path: String,
|
||||
folder_path: String,
|
||||
selection: AssetSelection,
|
||||
texture_id: Option<egui::TextureId>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug, Clone)]
|
||||
pub(crate) struct InspectorClipboard {
|
||||
component: Option<CopiedComponent>,
|
||||
@ -1311,6 +1320,112 @@ fn static_mesh_asset_ref_candidates(
|
||||
candidates
|
||||
}
|
||||
|
||||
fn texture_asset_candidates(world: &World) -> Vec<TextureAssetCandidate> {
|
||||
let Some(catalog) = world.get_resource::<EditorAssets>() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let snapshot = world
|
||||
.get_resource::<AssetThumbnailCache>()
|
||||
.map(AssetThumbnailCache::snapshot);
|
||||
let mut candidates: Vec<_> = catalog
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
|
||||
.filter_map(|asset| {
|
||||
let path = asset.path.clone()?;
|
||||
let texture_id = snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.texture_for(asset));
|
||||
Some(TextureAssetCandidate {
|
||||
label: asset.label.clone(),
|
||||
path: path.clone(),
|
||||
folder_path: asset.folder_path.clone(),
|
||||
selection: AssetSelection::File(path),
|
||||
texture_id,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.path.cmp(&b.path)));
|
||||
candidates
|
||||
}
|
||||
|
||||
fn request_texture_asset_thumbnails(world: &mut World) {
|
||||
let requests: Vec<_> = world
|
||||
.get_resource::<EditorAssets>()
|
||||
.map(|assets| {
|
||||
assets
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
|
||||
.filter_map(|asset| Some((asset_cache_key(asset), asset.path.clone()?)))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if requests.is_empty() || world.get_resource::<AssetThumbnailCache>().is_none() {
|
||||
return;
|
||||
}
|
||||
let asset_server = world.resource::<AssetServer>().clone();
|
||||
world.resource_scope(|_world, mut cache: Mut<AssetThumbnailCache>| {
|
||||
for (key, path) in requests {
|
||||
cache.request_texture(key, path, &asset_server);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn texture_path_from_selection(
|
||||
world: &World,
|
||||
selection: &AssetSelection,
|
||||
) -> Option<TextureAssetCandidate> {
|
||||
match selection {
|
||||
AssetSelection::File(path) => {
|
||||
let asset = world
|
||||
.get_resource::<EditorAssets>()?
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| {
|
||||
matches!(asset.kind, EditorAssetKind::Texture)
|
||||
&& asset.path.as_deref() == Some(path.as_str())
|
||||
})?;
|
||||
Some(TextureAssetCandidate {
|
||||
label: asset.label.clone(),
|
||||
path: path.clone(),
|
||||
folder_path: asset.folder_path.clone(),
|
||||
selection: selection.clone(),
|
||||
texture_id: None,
|
||||
})
|
||||
}
|
||||
AssetSelection::SubAsset {
|
||||
label,
|
||||
kind: AssetSubAssetKind::Texture,
|
||||
source_path: Some(path),
|
||||
..
|
||||
} => {
|
||||
if let Some(asset) = world.get_resource::<EditorAssets>().and_then(|assets| {
|
||||
assets
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.path.as_deref() == Some(path.as_str()))
|
||||
}) {
|
||||
return Some(TextureAssetCandidate {
|
||||
label: asset.label.clone(),
|
||||
path: path.clone(),
|
||||
folder_path: asset.folder_path.clone(),
|
||||
selection: selection.clone(),
|
||||
texture_id: None,
|
||||
});
|
||||
}
|
||||
Some(TextureAssetCandidate {
|
||||
label: label.clone(),
|
||||
path: path.clone(),
|
||||
folder_path: fallback_asset_folder(path),
|
||||
selection: selection.clone(),
|
||||
texture_id: None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn source_material_ref_for_part(
|
||||
asset_id: &str,
|
||||
part: &crate::assets::static_mesh::StaticMeshPart,
|
||||
@ -1424,14 +1539,8 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
let Some(mut renderer) = world.get::<StaticMeshRenderer>(entity).cloned() else {
|
||||
return;
|
||||
};
|
||||
let mut material_override = world
|
||||
.get::<MaterialOverride>(entity)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let original = renderer.clone();
|
||||
let original_override = material_override.clone();
|
||||
let mut changed = false;
|
||||
let mut override_changed = false;
|
||||
let mut remove_entry = None;
|
||||
|
||||
let card = component_card_context(
|
||||
@ -1495,7 +1604,7 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
|
||||
let material_response = asset_selector_row(
|
||||
ui,
|
||||
"Material",
|
||||
"Source material",
|
||||
icons::PALETTE,
|
||||
entry.material.as_ref(),
|
||||
inherited_material.as_ref(),
|
||||
@ -1522,12 +1631,6 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
.checkbox(&mut entry.receive_shadows, "Receive shadows")
|
||||
.changed();
|
||||
});
|
||||
override_changed |= material_override_slot_ui(
|
||||
ui,
|
||||
entry,
|
||||
inherited_material.as_ref(),
|
||||
&mut material_override,
|
||||
);
|
||||
};
|
||||
|
||||
if ui.available_width() < 430.0 {
|
||||
@ -1564,21 +1667,12 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
apply_component_card_response(world, entity, card_response);
|
||||
|
||||
if let Some(index) = remove_entry {
|
||||
if let Some(entry) = renderer.slots.get(index) {
|
||||
material_override
|
||||
.slots
|
||||
.retain(|slot| slot.slot_id != entry.id);
|
||||
override_changed = true;
|
||||
}
|
||||
renderer.slots.remove(index);
|
||||
changed = true;
|
||||
}
|
||||
if changed && renderer != original {
|
||||
set_static_mesh_renderer_with_history(world, entity, renderer);
|
||||
}
|
||||
if override_changed && material_override != original_override {
|
||||
set_material_override_with_history(world, entity, material_override);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_slot_id(entry: &mut StaticMeshRendererEntry, index: usize) {
|
||||
@ -1860,90 +1954,6 @@ fn asset_selector_control(
|
||||
);
|
||||
}
|
||||
|
||||
fn material_override_slot_ui(
|
||||
ui: &mut egui::Ui,
|
||||
entry: &StaticMeshRendererEntry,
|
||||
inherited_material: Option<&EditorAssetRef>,
|
||||
overrides: &mut MaterialOverride,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let index = overrides
|
||||
.slots
|
||||
.iter()
|
||||
.position(|slot| slot.slot_id == entry.id);
|
||||
let mut has_override = index.is_some();
|
||||
if ui
|
||||
.checkbox(&mut has_override, "Override material on this actor")
|
||||
.changed()
|
||||
{
|
||||
if has_override {
|
||||
overrides.slots.push(MaterialSlotOverride {
|
||||
slot_id: entry.id.clone(),
|
||||
base_material: entry
|
||||
.material
|
||||
.clone()
|
||||
.or_else(|| inherited_material.cloned()),
|
||||
material: MaterialDesc::default(),
|
||||
});
|
||||
} else if let Some(index) = index {
|
||||
overrides.slots.remove(index);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if has_override {
|
||||
if let Some(slot) = overrides
|
||||
.slots
|
||||
.iter_mut()
|
||||
.find(|slot| slot.slot_id == entry.id)
|
||||
{
|
||||
let effective_base = entry
|
||||
.material
|
||||
.clone()
|
||||
.or_else(|| inherited_material.cloned());
|
||||
if slot.base_material != effective_base {
|
||||
slot.base_material = effective_base;
|
||||
changed = true;
|
||||
}
|
||||
changed |= material_inline_ui(ui, &mut slot.material);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn material_inline_ui(ui: &mut egui::Ui, material: &mut MaterialDesc) -> bool {
|
||||
let mut changed = false;
|
||||
changed |= material_shader_ui(ui, material);
|
||||
let mut color = [
|
||||
material.base_color.r,
|
||||
material.base_color.g,
|
||||
material.base_color.b,
|
||||
material.base_color.a,
|
||||
];
|
||||
property_row(ui, "Override color", |ui| {
|
||||
if ui.color_edit_button_rgba_unmultiplied(&mut color).changed() {
|
||||
material.base_color = ColorDesc {
|
||||
r: color[0],
|
||||
g: color[1],
|
||||
b: color[2],
|
||||
a: color[3],
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
property_row(ui, "Metallic", |ui| {
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut material.metallic, 0.0..=1.0))
|
||||
.changed();
|
||||
});
|
||||
property_row(ui, "Roughness", |ui| {
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut material.roughness, 0.0..=1.0))
|
||||
.changed();
|
||||
});
|
||||
changed
|
||||
}
|
||||
|
||||
fn material_shader_ui(ui: &mut egui::Ui, material: &mut MaterialDesc) -> bool {
|
||||
let mut changed = false;
|
||||
property_row(ui, "Shader", |ui| {
|
||||
@ -2243,6 +2253,8 @@ pub fn material_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
.get::<MaterialDesc>(entity)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
request_texture_asset_thumbnails(world);
|
||||
let texture_candidates = texture_asset_candidates(world);
|
||||
let original = material.clone();
|
||||
let mut changed = false;
|
||||
let mut options = ComponentCardOptions::removable(
|
||||
@ -2312,13 +2324,33 @@ pub fn material_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
||||
.changed();
|
||||
});
|
||||
|
||||
changed |= option_string_ui(ui, "Base color texture", &mut material.base_color_texture);
|
||||
changed |= option_string_ui(ui, "Emissive texture", &mut material.emissive_texture);
|
||||
changed |= option_string_ui(ui, "Normal map", &mut material.normal_map_texture);
|
||||
changed |= option_string_ui(
|
||||
changed |= texture_asset_picker_ui(
|
||||
world,
|
||||
ui,
|
||||
"Base color texture",
|
||||
&mut material.base_color_texture,
|
||||
&texture_candidates,
|
||||
);
|
||||
changed |= texture_asset_picker_ui(
|
||||
world,
|
||||
ui,
|
||||
"Emissive texture",
|
||||
&mut material.emissive_texture,
|
||||
&texture_candidates,
|
||||
);
|
||||
changed |= texture_asset_picker_ui(
|
||||
world,
|
||||
ui,
|
||||
"Normal map",
|
||||
&mut material.normal_map_texture,
|
||||
&texture_candidates,
|
||||
);
|
||||
changed |= texture_asset_picker_ui(
|
||||
world,
|
||||
ui,
|
||||
"Metallic/roughness texture",
|
||||
&mut material.metallic_roughness_texture,
|
||||
&texture_candidates,
|
||||
);
|
||||
|
||||
if crate::assets::materials::material_asset_picker_ui(
|
||||
@ -2895,6 +2927,255 @@ fn has_scene_sun_override(world: &mut World) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn texture_asset_picker_ui(
|
||||
world: &mut World,
|
||||
ui: &mut egui::Ui,
|
||||
label: &str,
|
||||
value: &mut Option<String>,
|
||||
candidates: &[TextureAssetCandidate],
|
||||
) -> bool {
|
||||
let current_path = value.clone();
|
||||
let dragging_selection = world
|
||||
.get_resource::<EditorAssets>()
|
||||
.and_then(|assets| assets.dragging_selection().cloned());
|
||||
let drop_candidate = dragging_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| texture_path_from_selection(world, selection));
|
||||
let mut selected_path: Option<String> = None;
|
||||
let mut clear = false;
|
||||
let mut locate = false;
|
||||
let mut accepted_drop = false;
|
||||
let current_candidate = current_path.as_ref().and_then(|path| {
|
||||
candidates
|
||||
.iter()
|
||||
.find(|candidate| candidate.path == *path)
|
||||
.cloned()
|
||||
});
|
||||
|
||||
property_row(ui, label, |ui| {
|
||||
let control_width = ui
|
||||
.available_width()
|
||||
.max(MIN_INLINE_CONTROL_WIDTH.min(ui.available_width().max(1.0)));
|
||||
let row_height = 30.0;
|
||||
let (rect, _response) =
|
||||
ui.allocate_exact_size(egui::vec2(control_width, row_height), egui::Sense::hover());
|
||||
let valid_drag = drop_candidate.is_some();
|
||||
let row_hovered = ui.rect_contains_pointer(rect);
|
||||
let drop_hovered = valid_drag && row_hovered;
|
||||
let stroke = if drop_hovered {
|
||||
egui::Stroke::new(2.0, egui::Color32::from_rgb(125, 198, 255))
|
||||
} else if valid_drag {
|
||||
egui::Stroke::new(1.0, egui::Color32::from_rgb(58, 88, 122))
|
||||
} else if row_hovered {
|
||||
egui::Stroke::new(1.0, egui::Color32::from_rgb(92, 102, 118))
|
||||
} else {
|
||||
egui::Stroke::new(1.0, BORDER)
|
||||
};
|
||||
let fill = if drop_hovered {
|
||||
egui::Color32::from_rgb(29, 57, 86)
|
||||
} else if valid_drag {
|
||||
WIDGET_BG.linear_multiply(0.88)
|
||||
} else if row_hovered {
|
||||
WIDGET_BG.linear_multiply(1.05)
|
||||
} else {
|
||||
WIDGET_BG.linear_multiply(0.75)
|
||||
};
|
||||
ui.painter()
|
||||
.rect(rect, 4.0, fill, stroke, egui::StrokeKind::Inside);
|
||||
if drop_hovered {
|
||||
let badge_rect = egui::Rect::from_min_size(
|
||||
rect.right_top() + egui::vec2(-82.0, 4.0),
|
||||
egui::vec2(74.0, 16.0),
|
||||
);
|
||||
ui.painter().rect(
|
||||
badge_rect,
|
||||
3.0,
|
||||
egui::Color32::from_rgb(35, 95, 155),
|
||||
egui::Stroke::NONE,
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
ui.painter().text(
|
||||
badge_rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Drop texture",
|
||||
egui::FontId::new(10.0, egui::FontFamily::Proportional),
|
||||
egui::Color32::from_rgb(225, 241, 255),
|
||||
);
|
||||
}
|
||||
|
||||
let mut child = ui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(rect.shrink2(egui::vec2(6.0, 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
child.set_clip_rect(rect);
|
||||
let preview_size = 22.0;
|
||||
let (preview_rect, _preview_response) =
|
||||
child.allocate_exact_size(egui::vec2(preview_size, preview_size), egui::Sense::hover());
|
||||
child.painter().rect(
|
||||
preview_rect,
|
||||
3.0,
|
||||
PANEL_BG_DARK,
|
||||
egui::Stroke::new(1.0, BORDER),
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
if let Some(texture_id) = current_candidate
|
||||
.as_ref()
|
||||
.and_then(|candidate| candidate.texture_id)
|
||||
{
|
||||
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
|
||||
child.painter().image(
|
||||
texture_id,
|
||||
preview_rect.shrink(1.0),
|
||||
uv,
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
} else {
|
||||
child.painter().text(
|
||||
preview_rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
icons::IMAGE.as_str(),
|
||||
egui::FontId::new(12.0, egui::FontFamily::Name("phosphor-regular".into())),
|
||||
TEXT_DIM,
|
||||
);
|
||||
}
|
||||
|
||||
let action_width = 92.0;
|
||||
let text_width = (child.available_width() - action_width).max(1.0);
|
||||
child.allocate_ui_with_layout(
|
||||
egui::vec2(text_width, 22.0),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
let display_label = current_candidate
|
||||
.as_ref()
|
||||
.map(|candidate| candidate.label.as_str())
|
||||
.or(current_path.as_deref())
|
||||
.unwrap_or("(none)");
|
||||
ui.add_sized(
|
||||
[text_width, 20.0],
|
||||
egui::Label::new(egui::RichText::new(display_label).color(
|
||||
if current_path.is_some() {
|
||||
TEXT_DIM.linear_multiply(1.45)
|
||||
} else {
|
||||
TEXT_DIM
|
||||
},
|
||||
))
|
||||
.truncate(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
child.allocate_ui_with_layout(
|
||||
egui::vec2(action_width, 28.0),
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
let clear_response = ui.add_enabled(
|
||||
current_path.is_some(),
|
||||
egui::Button::new(phosphor_icon(icons::X, 16.0))
|
||||
.frame(false)
|
||||
.min_size(egui::vec2(22.0, 22.0)),
|
||||
);
|
||||
if clear_response.on_hover_text("Clear texture").clicked() {
|
||||
clear = true;
|
||||
}
|
||||
let locate_response = ui.add_enabled(
|
||||
current_path.is_some(),
|
||||
egui::Button::new(phosphor_icon(icons::CROSSHAIR, 16.0))
|
||||
.frame(false)
|
||||
.min_size(egui::vec2(22.0, 22.0)),
|
||||
);
|
||||
if locate_response
|
||||
.on_hover_text("Locate in content browser")
|
||||
.clicked()
|
||||
{
|
||||
locate = true;
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
ui.add_enabled(
|
||||
false,
|
||||
egui::Button::new(phosphor_icon(icons::FOLDER_OPEN, 16.0))
|
||||
.frame(false)
|
||||
.min_size(egui::vec2(22.0, 22.0)),
|
||||
)
|
||||
.on_hover_text("No texture assets available");
|
||||
} else {
|
||||
let menu = ui.menu_button(phosphor_icon(icons::FOLDER_OPEN, 16.0), |ui| {
|
||||
ui.set_min_width(240.0);
|
||||
for candidate in candidates {
|
||||
let selected = current_path.as_deref() == Some(candidate.path.as_str());
|
||||
if ui
|
||||
.selectable_label(selected, candidate.label.as_str())
|
||||
.on_hover_text(candidate.path.as_str())
|
||||
.clicked()
|
||||
{
|
||||
selected_path = Some(candidate.path.clone());
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
menu.response.on_hover_text("Browse textures");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if drop_hovered && ui.input(|input| input.pointer.any_released()) {
|
||||
if let Some(candidate) = drop_candidate.as_ref() {
|
||||
selected_path = Some(candidate.path.clone());
|
||||
accepted_drop = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if locate {
|
||||
locate_texture_asset(world, current_path.as_deref(), candidates);
|
||||
}
|
||||
if accepted_drop {
|
||||
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
|
||||
assets.clear_drag();
|
||||
}
|
||||
}
|
||||
if clear {
|
||||
*value = None;
|
||||
return current_path.is_some();
|
||||
}
|
||||
if let Some(path) = selected_path {
|
||||
let changed = current_path.as_deref() != Some(path.as_str());
|
||||
*value = Some(path);
|
||||
return changed;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn locate_texture_asset(
|
||||
world: &mut World,
|
||||
path: Option<&str>,
|
||||
candidates: &[TextureAssetCandidate],
|
||||
) {
|
||||
let Some(path) = path else {
|
||||
return;
|
||||
};
|
||||
let Some(candidate) = candidates.iter().find(|candidate| candidate.path == path) else {
|
||||
if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
|
||||
scene_io.status = format!("Could not locate texture {path}");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
|
||||
assets.current_folder = candidate.folder_path.clone();
|
||||
assets.select(candidate.selection.clone());
|
||||
}
|
||||
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
|
||||
let panel_nodes = ui_state.panel_nodes;
|
||||
open_and_focus_tab(
|
||||
&mut ui_state.dock_state,
|
||||
EditorTab::AssetBrowser,
|
||||
&panel_nodes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn option_string_ui(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) -> bool {
|
||||
let mut text = value.clone().unwrap_or_default();
|
||||
let before = text.clone();
|
||||
|
||||
@ -6,6 +6,9 @@ use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
||||
use egui_phosphor_icons::icons;
|
||||
use transform_gizmo_bevy::prelude::GizmoOptions;
|
||||
|
||||
use crate::assets::operators::{
|
||||
apply_material_operator, apply_texture_operator, place_asset_operator, place_subasset_operator,
|
||||
};
|
||||
use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssets};
|
||||
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
|
||||
use crate::render_target::ViewportRenderTarget;
|
||||
@ -545,7 +548,7 @@ fn viewport_asset_drop_ui(
|
||||
..
|
||||
}
|
||||
) {
|
||||
crate::assets::spawn_subasset_at(world, &selection, placement);
|
||||
place_subasset_operator(world, selection, placement);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -565,7 +568,7 @@ fn viewport_asset_drop_ui(
|
||||
.unwrap_or_else(|| crate::assets::ASSETS_ROOT.to_string()),
|
||||
kind: crate::assets::EditorAssetKind::Texture,
|
||||
};
|
||||
crate::ui::helpers::apply_texture_to_selection(world, &asset, selected);
|
||||
apply_texture_operator(world, asset, selected);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -578,15 +581,15 @@ fn viewport_asset_drop_ui(
|
||||
};
|
||||
match asset.kind {
|
||||
crate::assets::EditorAssetKind::Texture => {
|
||||
crate::ui::helpers::apply_texture_to_selection(world, &asset, selected);
|
||||
apply_texture_operator(world, asset, selected);
|
||||
return;
|
||||
}
|
||||
crate::assets::EditorAssetKind::Material => {
|
||||
crate::ui::helpers::apply_material_asset_to_selection(world, &asset, selected);
|
||||
apply_material_operator(world, asset, selected);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
crate::assets::spawn_asset_at(world, &asset, placement);
|
||||
place_asset_operator(world, asset, placement);
|
||||
}
|
||||
|
||||
@ -297,6 +297,10 @@ fn material_for_entry(
|
||||
parent_material: Option<&MaterialDesc>,
|
||||
material_override: Option<&MaterialOverride>,
|
||||
) -> Handle<StandardMaterial> {
|
||||
if let Some(parent_material) = parent_material {
|
||||
return materials.add(material_from_desc(asset_server, parent_material));
|
||||
}
|
||||
|
||||
if let Some(override_material) = material_override.and_then(|overrides| {
|
||||
overrides
|
||||
.slots
|
||||
@ -326,10 +330,6 @@ fn material_for_entry(
|
||||
return asset_server.load(labeled_asset_path(&source_path, label));
|
||||
}
|
||||
|
||||
if let Some(parent_material) = parent_material {
|
||||
return materials.add(material_from_desc(asset_server, parent_material));
|
||||
}
|
||||
|
||||
materials.add(StandardMaterial::default())
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# ADR 0018: Componentized actor inspector and material overrides
|
||||
# ADR 0018: Componentized actor inspector and materials
|
||||
|
||||
## Status
|
||||
|
||||
@ -35,17 +35,19 @@ Component footer rather than an ad hoc button near Transform.
|
||||
`ColliderDesc` and `RigidBodyDesc`. Mesh collider generation creates
|
||||
`ColliderDesc::StaticMesh` instead of renderer collider flags.
|
||||
- Materials are shader-schema aware. `MaterialDesc` stores a `ShaderRefDesc`,
|
||||
typed shader parameters, and texture bindings. Actor-level material edits are
|
||||
stored in `MaterialOverride`; shared material assets remain content-browser
|
||||
assets.
|
||||
typed shader parameters, texture bindings, StandardMaterial fields, and
|
||||
optional asset references. When an actor has an active `MaterialDesc`, static
|
||||
mesh hydration uses it before imported slot/source materials. Slot material
|
||||
references remain source/default selectors, not a separate actor material
|
||||
editing surface. Shared material assets remain content-browser assets.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New model placement creates `StaticMeshRenderer` plus optional
|
||||
`RigidBodyDesc`/`ColliderDesc`; legacy `PhysicsBody` remains loadable for old
|
||||
scenes.
|
||||
- Actor inspectors show imported asset selectors for mesh/material slots instead
|
||||
of source-path text fields.
|
||||
- Actor inspectors show imported asset selectors for mesh/source-material slots
|
||||
instead of source-path text fields.
|
||||
- Disabling a component in the inspector keeps its authoring data but prevents
|
||||
hydration/runtime systems from using it. Mesh, material, light, physics, and
|
||||
post-process hydration strip stale runtime components when active state
|
||||
|
||||
@ -42,14 +42,14 @@ 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/`.
|
||||
- **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.
|
||||
- **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 write `MaterialOverride` entries keyed by renderer slot ID; 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).
|
||||
- **Theme selections** use explicit high-contrast selected text and muted blue fills so hierarchy, asset browser, and inspector selection rows stay readable.
|
||||
- **Hierarchy** shows authored objects plus useful runtime context (`Player`, `PlayerCamera`, Project Sun) while keeping runtime rows read-only.
|
||||
- **Player placement** is authored through `PlayerSpawn`; selecting the runtime player in Edit mode redirects to a saved `Player Start`.
|
||||
- **World lighting** has two layers: project default sun/ambient settings, and optional scene-authored directional `LightDesc` overrides. **Scene → Lighting** menu and **Rendering** panel (Window) restore project sun or bake a scene directional from project settings. **Apply** in Project Settings updates ambient and sun illuminance live.
|
||||
- **Rendering** — `GiMode` + post-process volumes; see [rendering.md](rendering.md). **Window → Rendering** shows requested/effective active-camera stack, fallback reason, Solari eligibility, viewport GI badge, and volume HUD.
|
||||
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and an Add Component footer (`actor_inspector`) that expands an inline search shelf above the button; only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. The Add Component shelf is registry-driven with persistent search, category groups, descriptions, hydration hints, duplicate/conflict disabled states, and recommendation hints. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
||||
- **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.
|
||||
- **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`.
|
||||
|
||||
@ -164,7 +164,9 @@ Structural edits (spawn, delete, transform, rename) go through `EditorHistory` c
|
||||
User-facing actions route through the operator lifecycle in `operators.rs`: begin, preview,
|
||||
commit, cancel, disabled reason, and status text. The first bridge wraps command-palette and queued
|
||||
editor commands as immediate operators, while existing `EditorHistory` commands remain the undo
|
||||
payload. Modal tools and asset drops should use the same operator status path as they migrate.
|
||||
payload. Asset placement, mesh-subasset placement, texture apply, and material apply actions now
|
||||
use immediate operators from `assets::operators`; modal tools should use the same status path as
|
||||
they migrate.
|
||||
|
||||
PIE stop restores player simulation state only; authored `LevelObject` edits made during PIE remain in the scene.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user