Compare commits

..

No commits in common. "3794faa4aae8d910b6af58a9e470ddd808339cba" and "3931de1f0a241c9acb90baae006364962ed9f8c0" have entirely different histories.

26 changed files with 150 additions and 2848 deletions

View File

@ -8,26 +8,21 @@ present; this slice completes the production-facing catalog and drag/drop workfl
## Status ## Status
Implemented on `codex/material-library-targeted-drop` after collaborative-safety `#49` was Source audit complete. Implementation starts on a clean branch after collaborative-safety `#49` is
published and integrated into `main`. Focused and full source-only verification plus live editor published, because both slices touch the Asset Browser and should remain independently reviewable.
acceptance are green; ticket evidence remains before `#16` / `#18` close.
## Resolved Gaps ## Current Gaps
- The dockable Material Library now provides cross-folder Material/Instance catalog, usage counts, - Material assets are edited inside generic Asset Browser Details; no dedicated Material Library or
health, creation, thumbnails, drag sources, and the guarded existing details editors. scene-usage view exists.
- Viewport Material/Texture drops resolve the surface under the pointer and never broaden to the - Viewport material/texture drops act on the existing selection instead of the surface under the
current selection or every renderer slot. pointer.
- `HydratedRendererMaterialBinding` identifies the exact static/skinned renderer slot; primitives - Multi-slot renderers silently assign all slots; the hydrated draw hit already carries
and stable brush-face IDs use their authored component paths. `HydratedRendererMaterialBinding`, but the drop path does not use it.
- Unsupported, locked, linked, missing, and payload-incompatible targets reject with an explicit - Primitive and brush-face targeting is not explicit, and unsupported authored actors can receive a
reason and no scene mutation. generic `MaterialDesc` instead of an actionable rejection.
- Hover previews are reversible, target changes restore the prior snapshot, and release records one - Drag UI describes an eventual action but does not preview the actual affected surface or restore a
typed operator/history transaction. preview on cancel/target change.
- Viewport pointer tracking remains active while another egui widget owns the drag, so exact target
resolution cannot stall at the viewport boundary.
- Visibility hierarchy setup now precedes generated hydration content, preventing Bevy parent/child
visibility warnings when brush draws are created after scene load.
## Material Library ## Material Library
@ -78,17 +73,12 @@ menu; a viewport hit never silently broadens from one slot to all slots.
## Verification ## Verification
- Focused tests cover exact multi-slot assignment/undo, brush-face-only preview/restore, primitive - Unit-test ray-hit to authored target/slot mapping, primitive/face targeting, and invalid reasons.
Material/Texture resolution, renderer Texture rejection, Material Library filtering, and scene - Use `OperatorInvariantHarness` for preview target changes, release commit, Escape/outside cancel,
usage counts. dirty-state preservation, helper cleanup, and one-step undo/redo.
- Source checks include editor all-target check, focused tests, and strict editor Clippy. - Cover static one/multi-slot, skinned multi-slot, primitive, brush face, Texture rejection on a
- Live acceptance covered the docked catalog, search/type/usage filters, grid/list views, resolved renderer slot, missing material, and linked/locked prefab boundaries.
Material Instance thumbnails, exact primitive preview, green actor/surface feedback, one-step - Verify Material Library filtering, scene-usage counts, dependency diagnostics, and drag sources in
commit/undo, and Escape rollback. The same relaunch produced no hydration hierarchy warnings. headless egui tests.
- Static/skinned exact-slot, brush-face, locked/linked, incompatible Texture, and non-dirty preview - Run full source/headless checks. Packaged testing remains deferred until the project owner requests
contracts remain covered by focused automated tests; shared-asset file creation was deliberately it again; live visual acceptance is still required before closing `#16`/`#18`.
not invoked during live QA to avoid adding throwaway authored files.
- Full verification passed: workspace all-target check, strict workspace Clippy, all workspace tests,
strict editor Clippy with both `dev` and `hot-reload`, level validation with zero blockers, and
navigation artifact freshness. Packaged testing remains deferred until the project owner requests
it again.

View File

@ -152,9 +152,6 @@ deep-stale variants.
| Asset Browser project/file views | Browse `assets/`, search/filter/sort models, textures, materials, audio clips, levels, and prefabs; switch grid/list; expand model subassets; inspect file details; audition audio; drag supported assets/submeshes into the viewport | | Asset Browser project/file views | Browse `assets/`, search/filter/sort models, textures, materials, audio clips, levels, and prefabs; switch grid/list; expand model subassets; inspect file details; audition audio; drag supported assets/submeshes into the viewport |
| Drag audio clip into viewport | Create an authored audio source; when an audio source is selected, assign the clip instead | | Drag audio clip into viewport | Create an authored audio source; when an audio source is selected, assign the clip instead |
| Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` | | Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` |
| Window → Material Library | Search/filter project Materials and Material Instances, inspect scene usage and dependency health, create/edit shared assets, and drag them into the viewport |
| Drag Material/Instance onto viewport surface | Preview and assign the exact renderer slot, primitive, or brush face under the pointer; release commits one undo step, while Escape/right-click/outside restores the preview |
| Drag Texture onto viewport surface | Set a primitive base-color texture or exact brush face; renderer slots reject loose textures and direct you to a Material Instance |
| Static/Skinned Mesh Renderer material slots | Assign a shared Material/Material Instance per slot; Browse/Select/Locate the reference, or Clear the override to restore the imported source material | | Static/Skinned Mesh Renderer material slots | Assign a shared Material/Material Instance per slot; Browse/Select/Locate the reference, or Clear the override to restore the imported source material |
| `Ctrl+P` | Centered command palette; search human labels or stable command IDs, use arrow keys to select, Enter to run | | `Ctrl+P` | Centered command palette; search human labels or stable command IDs, use arrow keys to select, Enter to run |
| `F7` | While paused in Play: advance one sim tick | | `F7` | While paused in Play: advance one sim tick |
@ -437,7 +434,6 @@ crates/
- [x] FBX/glTF model import + normalized static/skinned renderer routing; explicit generic scene-instance load via `bevy_ufbx` / `ModelRef` - [x] FBX/glTF model import + normalized static/skinned renderer routing; explicit generic scene-instance load via `bevy_ufbx` / `ModelRef`
- [x] Asset browser model thumbnails (unified `assets/thumbnails/` pipeline; `ThumbnailState` cache; FBX via `FbxThumbnailSource`) - [x] Asset browser model thumbnails (unified `assets/thumbnails/` pipeline; `ThumbnailState` cache; FBX via `FbxThumbnailSource`)
- [x] Shared Material/Material Instance assets, stable static/skinned renderer material slots, imported-source fallback, orphan preservation, and runtime-only property blocks ([ADR 0035](docs/adr/0035-shared-material-assets-and-renderer-slots.md), [material-system guide](docs/editor/material-system.md)) - [x] Shared Material/Material Instance assets, stable static/skinned renderer material slots, imported-source fallback, orphan preservation, and runtime-only property blocks ([ADR 0035](docs/adr/0035-shared-material-assets-and-renderer-slots.md), [material-system guide](docs/editor/material-system.md))
- [x] Docked Material Library with cross-folder type/usage filters, resolved Material Instance thumbnails, shared guarded editors, and exact reversible viewport Material/Texture drops for renderer slots, primitives, and brush faces ([material-system guide](docs/editor/material-system.md); [Gitea #16](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/16), [Gitea #18](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/18))
- [x] Prefab v2 core: shared stable nested override paths, property/component/structural scopes, recursive graph validation, linked-root variants, conflict recovery, transactional source Apply, and undoable unpack/convert ([ADR 0027](docs/adr/0027-stable-prefab-ownership-and-variants.md)) - [x] Prefab v2 core: shared stable nested override paths, property/component/structural scopes, recursive graph validation, linked-root variants, conflict recovery, transactional source Apply, and undoable unpack/convert ([ADR 0027](docs/adr/0027-stable-prefab-ownership-and-variants.md))
- [x] Prefab v2 production acceptance: committed base/nested/variant fixtures pass workspace tests, recursive headless validation, packaged release startup, and live editor placement/inspection regression coverage ([Gitea #43](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/43)) - [x] Prefab v2 production acceptance: committed base/nested/variant fixtures pass workspace tests, recursive headless validation, packaged release startup, and live editor placement/inspection regression coverage ([Gitea #43](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/43))
- [x] Shared project validation: editor Diagnostics and `cargo validate-levels --json` use one owner-attributed dependency/finding report across project settings, registry/import artifacts, materials, shaders, scenes, prefabs, brushes, and colliders; valid/missing/cyclic/incompatible fixtures fail on blocking content errors ([ADR 0028](docs/adr/0028-authoritative-project-content-validation.md), [Gitea #45](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/45)) - [x] Shared project validation: editor Diagnostics and `cargo validate-levels --json` use one owner-attributed dependency/finding report across project settings, registry/import artifacts, materials, shaders, scenes, prefabs, brushes, and colliders; valid/missing/cyclic/incompatible fixtures fail on blocking content errors ([ADR 0028](docs/adr/0028-authoritative-project-content-validation.md), [Gitea #45](https://git.spacetrainclubhouse.com/Falling-Metal-Interactive/Blacksite/issues/45))
@ -480,7 +476,7 @@ crates/
**Unpack Layer** preserves nested links; **Convert to Local** recursively removes them. Current **Unpack Layer** preserves nested links; **Convert to Local** recursively removes them. Current
authoring UI coverage and production-acceptance gaps are tracked in authoring UI coverage and production-acceptance gaps are tracked in
[prefab-authoring.md](docs/editor/prefab-authoring.md). [prefab-authoring.md](docs/editor/prefab-authoring.md).
- Material and direct-base Material Instance assets live under `assets/materials/`; shader schemas live under `assets/shaders/`. **Window > Material Library** provides cross-folder creation, editing, usage filters, and drag sources. Viewport Material drops target one exact renderer slot, primitive, or brush face and use reversible preview plus one-step history; loose Textures target primitives/brush faces and are rejected on renderer slots. Shared edits propagate through live-updated handles without reloading skinned geometry. Custom Surface evaluators share one constrained ABI between raster and Solari-eligible non-deformed geometry. Dynamic skinned/morph Solari geometry and a persisted property-block authoring workflow remain future work; see the [material-system guide](docs/editor/material-system.md). - Material and direct-base Material Instance assets live under `assets/materials/`; shader schemas live under `assets/shaders/`. In the Asset Browser, select a Material and use **Create Instance** for sparse inherited variants. Static and skinned renderers own stable per-draw material slots, with explicit assignments taking precedence over imported source defaults; shared edits propagate through live-updated handles without reloading skinned geometry. Custom Surface evaluators share one constrained ABI between raster and Solari-eligible non-deformed geometry. Dynamic skinned/morph Solari geometry and a persisted property-block authoring workflow remain future work; see the [material-system guide](docs/editor/material-system.md).
- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path. - 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, - 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. material graphs, lighting profiles) can be added without changing the scene format foundation.

View File

@ -16,7 +16,10 @@ pub fn load_material_asset(catalog_path: &str) -> Result<MaterialAsset, String>
} }
pub fn material_desc_from_asset(catalog_path: &str) -> Result<MaterialDesc, String> { pub fn material_desc_from_asset(catalog_path: &str) -> Result<MaterialDesc, String> {
shared::load_resolved_material_from_path(catalog_path).map(|(desc, _)| desc) let asset = load_material_asset(catalog_path)?;
let mut desc = asset.material;
desc.material_asset_path = Some(catalog_path.to_string());
Ok(desc)
} }
pub fn apply_material_asset_to_selection( pub fn apply_material_asset_to_selection(
@ -107,7 +110,7 @@ pub fn apply_material_asset_to_selection(
Ok(()) Ok(())
} }
pub(crate) fn ensure_static_material_slots(renderer: &mut shared::StaticMeshRenderer) { fn ensure_static_material_slots(renderer: &mut shared::StaticMeshRenderer) {
for part in &mut renderer.slots { for part in &mut renderer.slots {
if part.material_slot_id.0.trim().is_empty() { if part.material_slot_id.0.trim().is_empty() {
part.material_slot_id = if part.id.0.trim().is_empty() { part.material_slot_id = if part.id.0.trim().is_empty() {

View File

@ -13,8 +13,8 @@ pub mod thumbnails;
pub use catalog::*; pub use catalog::*;
pub use thumbnails::{ pub use thumbnails::{
draw_asset_cell_with, gltf_skinned_primitive_labels, invalidate_on_catalog_refresh, kind_icon, draw_asset_cell_with, gltf_skinned_primitive_labels, invalidate_on_catalog_refresh, kind_icon,
prefetch_asset_thumbnails, prefetch_folder_thumbnails, AssetThumbnailCache, prefetch_folder_thumbnails, AssetThumbnailCache, ThumbnailCacheSnapshot, ThumbnailState,
ThumbnailCacheSnapshot, ThumbnailState, ThumbnailStudio, ThumbnailsPlugin, ThumbnailStudio, ThumbnailsPlugin,
}; };
/// Refreshes both generated contracts derived from one imported model source. /// Refreshes both generated contracts derived from one imported model source.

View File

@ -188,24 +188,18 @@ impl AssetThumbnailCache {
{ {
return; return;
} }
let material = match shared::load_resolved_material_from_path(&path) { let material = match shared::MaterialAsset::load_from_path(&path) {
Ok((material, _)) => material, Ok(asset) => asset,
Err(error) => { Err(error) => {
self.mark_studio_failed(&key, &error, true); self.mark_studio_failed(&key, &error, true);
return; return;
} }
}; };
let label = shared::MaterialAsset::load_from_path(&path)
.map(|asset| asset.label)
.or_else(|_| {
shared::MaterialInstanceAsset::load_from_path(&path).map(|asset| asset.label)
})
.unwrap_or_else(|_| "Material".into());
if studio.enqueue_source( if studio.enqueue_source(
key.clone(), key.clone(),
ThumbnailJobSource::MaterialAsset { ThumbnailJobSource::MaterialAsset {
label, label: material.label.clone(),
material: Box::new(material), material: Box::new(material.material),
}, },
) { ) {
self.studio_pending.insert(key); self.studio_pending.insert(key);
@ -488,37 +482,6 @@ pub fn prefetch_folder_thumbnails(world: &mut World, folder: &str) {
.prefetched_folder = Some(folder.to_string()); .prefetched_folder = Some(folder.to_string());
} }
/// Requests thumbnails for an explicit cross-folder asset set.
pub fn prefetch_asset_thumbnails(world: &mut World, assets: &[EditorAsset]) {
let requests = assets
.iter()
.filter_map(|asset| {
Some((
asset_cache_key(asset),
asset.path.clone()?,
asset.kind.clone(),
))
})
.collect::<Vec<_>>();
let asset_server = world.resource::<AssetServer>().clone();
world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
world.resource_scope(|_world, mut studio: Mut<ThumbnailStudio>| {
for (key, path, kind) in requests {
match kind {
EditorAssetKind::Texture => cache.request_texture(key, path, &asset_server),
EditorAssetKind::Model => {
cache.request_model(key, path, &asset_server, &mut studio)
}
EditorAssetKind::Material => {
cache.request_material_asset(key, path, &mut studio)
}
_ => {}
}
}
});
});
}
pub fn invalidate_on_catalog_refresh(world: &mut World) { pub fn invalidate_on_catalog_refresh(world: &mut World) {
world.resource_mut::<AssetThumbnailCache>().invalidate_all(); world.resource_mut::<AssetThumbnailCache>().invalidate_all();
if !world.contains_resource::<ThumbnailStudio>() { if !world.contains_resource::<ThumbnailStudio>() {

View File

@ -8,9 +8,8 @@ mod sources;
mod studio; mod studio;
pub use cache::{ pub use cache::{
draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_asset_thumbnails, draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails,
prefetch_folder_thumbnails, AssetThumbnailCache, AssetThumbnailsPlugin, ThumbnailCacheSnapshot, AssetThumbnailCache, AssetThumbnailsPlugin, ThumbnailCacheSnapshot, ThumbnailState,
ThumbnailState,
}; };
pub use job::{ThumbnailJob, ThumbnailJobSource}; pub use job::{ThumbnailJob, ThumbnailJobSource};
pub use sources::{ pub use sources::{

View File

@ -37,7 +37,6 @@ pub use viewport::brush_edit;
pub use viewport::brush_tool; pub use viewport::brush_tool;
pub use viewport::camera; pub use viewport::camera;
pub use viewport::gizmos; pub use viewport::gizmos;
pub use viewport::material_drop;
pub use viewport::render_view; pub use viewport::render_view;
pub use viewport::rendering_diagnostics; pub use viewport::rendering_diagnostics;
pub use viewport::selection; pub use viewport::selection;
@ -63,7 +62,6 @@ use extensibility::ExtensibilityPlugin;
use gizmos::EditorGizmoPlugin; use gizmos::EditorGizmoPlugin;
use history::EditorHistoryPlugin; use history::EditorHistoryPlugin;
use infra::EditorInfraPlugin; use infra::EditorInfraPlugin;
use material_drop::MaterialDropPlugin;
use operators::OperatorPlugin; use operators::OperatorPlugin;
use play::audio_preview::AudioPreviewPlugin; use play::audio_preview::AudioPreviewPlugin;
use play::PlaySessionPlugin; use play::PlaySessionPlugin;
@ -109,7 +107,6 @@ impl PluginGroup for EditorPluginGroup {
.add(EditorCameraPlugin) .add(EditorCameraPlugin)
.add(AudioPreviewPlugin) .add(AudioPreviewPlugin)
.add(ActorIconsPlugin) .add(ActorIconsPlugin)
.add(MaterialDropPlugin)
.add(EditorSelectionPlugin) .add(EditorSelectionPlugin)
.add(SelectionOutlinePlugin) .add(SelectionOutlinePlugin)
.add(EditorGizmoPlugin) .add(EditorGizmoPlugin)

View File

@ -1855,7 +1855,7 @@ fn details_panel(world: &mut World, ui: &mut egui::Ui, selected_entities: &Selec
} }
} }
pub(crate) fn top_level_asset_details_panel( fn top_level_asset_details_panel(
world: &mut World, world: &mut World,
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_entities: &SelectedEntities, selected_entities: &SelectedEntities,
@ -2524,11 +2524,7 @@ fn save_material_draft(world: &mut World) {
} }
} }
pub(crate) fn create_material_instance_from_base( fn create_material_instance_from_base(world: &mut World, base_path: &str, base_label: &str) {
world: &mut World,
base_path: &str,
base_label: &str,
) {
let base_reference = world let base_reference = world
.get_resource_mut::<crate::asset_db::AssetRegistry>() .get_resource_mut::<crate::asset_db::AssetRegistry>()
.ok_or_else(|| "Asset registry is unavailable".to_string()) .ok_or_else(|| "Asset registry is unavailable".to_string())

View File

@ -36,10 +36,7 @@ impl PanelNodes {
EditorTab::Viewport | EditorTab::GameView => self.main_view(), EditorTab::Viewport | EditorTab::GameView => self.main_view(),
EditorTab::Hierarchy => self.hierarchy(), EditorTab::Hierarchy => self.hierarchy(),
EditorTab::Inspector => self.inspector(), EditorTab::Inspector => self.inspector(),
EditorTab::AssetBrowser EditorTab::AssetBrowser | EditorTab::Build | EditorTab::Toolbar => self.bottom(),
| EditorTab::MaterialLibrary
| EditorTab::Build
| EditorTab::Toolbar => self.bottom(),
} }
} }
@ -54,22 +51,20 @@ impl PanelNodes {
EditorTab::Viewport | EditorTab::GameView => nodes.main_view = path.node.0, EditorTab::Viewport | EditorTab::GameView => nodes.main_view = path.node.0,
EditorTab::Hierarchy => nodes.hierarchy = path.node.0, EditorTab::Hierarchy => nodes.hierarchy = path.node.0,
EditorTab::Inspector => nodes.inspector = path.node.0, EditorTab::Inspector => nodes.inspector = path.node.0,
EditorTab::AssetBrowser EditorTab::AssetBrowser | EditorTab::Build | EditorTab::Toolbar => {
| EditorTab::MaterialLibrary nodes.bottom = path.node.0
| EditorTab::Build }
| EditorTab::Toolbar => nodes.bottom = path.node.0,
} }
} }
nodes nodes
} }
} }
pub const PANEL_TABS: [EditorTab; 6] = [ pub const PANEL_TABS: [EditorTab; 5] = [
EditorTab::Viewport, EditorTab::Viewport,
EditorTab::Hierarchy, EditorTab::Hierarchy,
EditorTab::Inspector, EditorTab::Inspector,
EditorTab::AssetBrowser, EditorTab::AssetBrowser,
EditorTab::MaterialLibrary,
EditorTab::Build, EditorTab::Build,
]; ];
@ -79,7 +74,6 @@ pub fn tab_label(tab: &EditorTab) -> &'static str {
EditorTab::Hierarchy => "Hierarchy", EditorTab::Hierarchy => "Hierarchy",
EditorTab::Inspector => "Inspector", EditorTab::Inspector => "Inspector",
EditorTab::AssetBrowser => "Asset Browser", EditorTab::AssetBrowser => "Asset Browser",
EditorTab::MaterialLibrary => "Material Library",
EditorTab::Build => "Build", EditorTab::Build => "Build",
EditorTab::Toolbar => "Toolbar (legacy)", EditorTab::Toolbar => "Toolbar (legacy)",
} }

View File

@ -32,11 +32,8 @@ pub fn default_dock_layout() -> DockLayoutSnapshot {
tree.split_right(NodeIndex::root(), 0.76, vec![EditorTab::Inspector]); tree.split_right(NodeIndex::root(), 0.76, vec![EditorTab::Inspector]);
let [main_view_node, hierarchy_node] = let [main_view_node, hierarchy_node] =
tree.split_left(main_view_node, 0.2, vec![EditorTab::Hierarchy]); tree.split_left(main_view_node, 0.2, vec![EditorTab::Hierarchy]);
let [main_view_node, bottom_node] = tree.split_below( let [main_view_node, bottom_node] =
main_view_node, tree.split_below(main_view_node, 0.88, vec![EditorTab::AssetBrowser]);
0.88,
vec![EditorTab::AssetBrowser, EditorTab::MaterialLibrary],
);
let panel_nodes = PanelNodes { let panel_nodes = PanelNodes {
main_view: main_view_node.0, main_view: main_view_node.0,

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,6 @@ pub mod hierarchy_ops;
pub mod hierarchy_state; pub mod hierarchy_state;
pub(crate) mod inspector; pub(crate) mod inspector;
mod layout; mod layout;
mod material_library;
mod menu; mod menu;
pub(crate) mod navigation_inspector; pub(crate) mod navigation_inspector;
mod play_controls; mod play_controls;
@ -76,9 +75,6 @@ pub struct UiState {
last_mode_tab: Option<EditorMode>, last_mode_tab: Option<EditorMode>,
} }
#[derive(Resource, Default)]
struct DockTabRequest(Option<EditorTab>);
pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool { pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool {
ctx.egui_wants_keyboard_input() ctx.egui_wants_keyboard_input()
} }
@ -191,10 +187,6 @@ impl UiState {
.show_inside(root_ui, &mut viewer); .show_inside(root_ui, &mut viewer);
}); });
if let Some(tab) = world.resource_mut::<DockTabRequest>().0.take() {
dock_tabs::open_and_focus_tab(&mut self.dock_state, tab, &self.panel_nodes);
}
self.panel_nodes = PanelNodes::discover(&self.dock_state, self.panel_nodes); self.panel_nodes = PanelNodes::discover(&self.dock_state, self.panel_nodes);
mark_layout_dirty_if_changed( mark_layout_dirty_if_changed(
@ -291,7 +283,6 @@ pub enum EditorTab {
Inspector, Inspector,
Toolbar, Toolbar,
AssetBrowser, AssetBrowser,
MaterialLibrary,
Build, Build,
} }
@ -307,9 +298,6 @@ impl Plugin for EditorUiPlugin {
.init_resource::<component_registry::EditorComponentRegistry>() .init_resource::<component_registry::EditorComponentRegistry>()
.init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>() .init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
.init_resource::<asset_browser::AssetBrowserUiState>() .init_resource::<asset_browser::AssetBrowserUiState>()
.init_resource::<material_library::MaterialLibraryState>()
.init_resource::<material_library::MaterialLibraryCatalog>()
.init_resource::<DockTabRequest>()
.init_resource::<inspector::InspectorClipboard>() .init_resource::<inspector::InspectorClipboard>()
.init_resource::<inspector::InspectorPanelState>() .init_resource::<inspector::InspectorPanelState>()
.init_resource::<animation_inspector::AnimationInspectorState>() .init_resource::<animation_inspector::AnimationInspectorState>()
@ -327,10 +315,6 @@ impl Plugin for EditorUiPlugin {
apply_ui_state_from_prefs apply_ui_state_from_prefs
.after(crate::project_io::load_user_preferences_on_startup), .after(crate::project_io::load_user_preferences_on_startup),
) )
.add_systems(
EguiPrimaryContextPass,
material_library::refresh_material_library_catalog.before(show_ui_system),
)
.add_systems(EguiPrimaryContextPass, show_ui_system) .add_systems(EguiPrimaryContextPass, show_ui_system)
.add_systems( .add_systems(
Update, Update,
@ -437,9 +421,6 @@ impl egui_dock::TabViewer for TabViewer<'_> {
EditorTab::AssetBrowser => { EditorTab::AssetBrowser => {
asset_browser::asset_browser_ui(self.world, ui, self.selected_entities); asset_browser::asset_browser_ui(self.world, ui, self.selected_entities);
} }
EditorTab::MaterialLibrary => {
material_library::material_library_ui(self.world, ui, self.selected_entities);
}
EditorTab::Build => build::build_ui(self.world, ui), EditorTab::Build => build::build_ui(self.world, ui),
} }
} }
@ -451,7 +432,6 @@ impl egui_dock::TabViewer for TabViewer<'_> {
EditorTab::Inspector => (icons::SLIDERS, "Inspector"), EditorTab::Inspector => (icons::SLIDERS, "Inspector"),
EditorTab::Toolbar => (icons::WRENCH, "Toolbar"), EditorTab::Toolbar => (icons::WRENCH, "Toolbar"),
EditorTab::AssetBrowser => (icons::FOLDER, "Asset Browser"), EditorTab::AssetBrowser => (icons::FOLDER, "Asset Browser"),
EditorTab::MaterialLibrary => (icons::PALETTE, "Material Library"),
EditorTab::Build => (icons::HAMMER, "Build"), EditorTab::Build => (icons::HAMMER, "Build"),
}; };
tab_title(icon, label) tab_title(icon, label)

View File

@ -7,8 +7,8 @@ use egui_phosphor_icons::icons;
use transform_gizmo_bevy::prelude::GizmoOptions; use transform_gizmo_bevy::prelude::GizmoOptions;
use crate::assets::operators::{ use crate::assets::operators::{
assign_animation_clip_operator, assign_audio_clip_operator, place_asset_operator, apply_material_operator, apply_texture_operator, assign_animation_clip_operator,
place_subasset_operator, assign_audio_clip_operator, place_asset_operator, place_subasset_operator,
}; };
use crate::assets::thumbnails::kind_icon; use crate::assets::thumbnails::kind_icon;
use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssets}; use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssets};
@ -21,8 +21,8 @@ use crate::viewport::actor_icons::ActorIconSettings;
use crate::viewport::brush_edit::{BrushEditMode, BrushElementSelection}; use crate::viewport::brush_edit::{BrushEditMode, BrushElementSelection};
use crate::viewport::brush_tool::{BrushToolPhase, BrushToolState}; use crate::viewport::brush_tool::{BrushToolPhase, BrushToolState};
use crate::viewport::{ use crate::viewport::{
material_drop::is_surface_asset_selection, snap_translation, viewport_ground_position, snap_translation, viewport_ground_position, EditorViewportMode, ViewportDisplayMode,
EditorViewportMode, MaterialDropState, ViewportDisplayMode, ViewportSettings, ViewportSettings,
}; };
use super::helpers::{ use super::helpers::{
@ -30,8 +30,7 @@ use super::helpers::{
}; };
use super::theme::{ use super::theme::{
overlay_chip_frame, viewport_toolbar_frame, ACCENT, ACCENT_HOVER, AXIS_X, AXIS_Y, AXIS_Z, overlay_chip_frame, viewport_toolbar_frame, ACCENT, ACCENT_HOVER, AXIS_X, AXIS_Y, AXIS_Z,
BORDER, BORDER_STRONG, ELEVATED_BG, ERROR, SELECTION, SUCCESS, TEXT, TEXT_DIM, TEXT_MUTED, BORDER, BORDER_STRONG, ELEVATED_BG, SELECTION, SUCCESS, TEXT, TEXT_DIM, TEXT_MUTED, WARNING,
WARNING,
}; };
use super::widgets::{ use super::widgets::{
icon_button_small, panel_toolbar_row, phosphor_icon, tool_button, tool_button_accent, icon_button_small, panel_toolbar_row, phosphor_icon, tool_button, tool_button_accent,
@ -70,17 +69,9 @@ pub fn viewport_tab_ui(
let clean_game_view = world.resource::<ViewportDisplayMode>().clean_game_view; let clean_game_view = world.resource::<ViewportDisplayMode>().clean_game_view;
let response = ui.allocate_rect(rect, egui::Sense::HOVER); let response = ui.allocate_rect(rect, egui::Sense::HOVER);
// A drag source owns egui's interaction response, so `Response::hover_pos()` *viewport_pointer_pos = response
// is intentionally empty while an asset is dragged over the viewport. The .interact_pointer_pos()
// global pointer position remains available and is the correct input for .or_else(|| response.hover_pos())
// drag previews and exact surface targeting.
*viewport_pointer_pos = ui
.input(|input| {
input
.pointer
.interact_pos()
.or_else(|| input.pointer.hover_pos())
})
.filter(|pos| rect.contains(*pos)); .filter(|pos| rect.contains(*pos));
*pointer_in_viewport = viewport_pointer_pos.is_some(); *pointer_in_viewport = viewport_pointer_pos.is_some();
@ -829,44 +820,11 @@ fn viewport_asset_drop_ui(
.resource::<EditorAssets>() .resource::<EditorAssets>()
.dragging_selection() .dragging_selection()
.cloned(); .cloned();
let surface_drag = drag_selection let drag_descriptor = drag_selection
.as_ref()
.is_some_and(|selection| is_surface_asset_selection(world, selection));
let mut drag_descriptor = drag_selection
.as_ref() .as_ref()
.and_then(|selection| asset_drag_descriptor(world, selection, selected)); .and_then(|selection| asset_drag_descriptor(world, selection, selected));
let over_viewport = pointer_pos.is_some_and(|pos| viewport_rect.contains(pos)); let over_viewport = pointer_pos.is_some_and(|pos| viewport_rect.contains(pos));
let surface_feedback = surface_drag
.then(|| world.get_resource::<MaterialDropState>())
.flatten()
.and_then(MaterialDropState::feedback)
.cloned();
if surface_drag && over_viewport {
if let Some((_, _, action)) = drag_descriptor.as_mut() {
*action = surface_feedback
.as_ref()
.map(|feedback| feedback.action.clone())
.unwrap_or_else(|| "Finding an assignable surface".into());
}
}
let drop_valid = (surface_drag && over_viewport)
.then(|| surface_feedback.as_ref().map(|feedback| feedback.valid))
.flatten();
let highlight_color = match drop_valid {
Some(true) => SUCCESS,
Some(false) => ERROR,
None => ACCENT_HOVER,
};
let drag_visual_state = if !over_viewport {
AssetDragVisualState::Outside
} else {
match drop_valid {
Some(true) => AssetDragVisualState::Valid,
Some(false) => AssetDragVisualState::Invalid,
None => AssetDragVisualState::Targeted,
}
};
if let (Some(pointer), Some((icon, label, action))) = (pointer_pos, drag_descriptor.as_ref()) { if let (Some(pointer), Some((icon, label, action))) = (pointer_pos, drag_descriptor.as_ref()) {
draw_asset_drag_preview( draw_asset_drag_preview(
ui, ui,
@ -875,7 +833,7 @@ fn viewport_asset_drop_ui(
*icon, *icon,
label, label,
action, action,
drag_visual_state, over_viewport,
); );
} }
@ -883,7 +841,7 @@ fn viewport_asset_drop_ui(
ui.painter().rect_stroke( ui.painter().rect_stroke(
viewport_rect.shrink(2.0), viewport_rect.shrink(2.0),
0.0, 0.0,
egui::Stroke::new(2.0, highlight_color), egui::Stroke::new(2.0, ACCENT_HOVER),
egui::StrokeKind::Inside, egui::StrokeKind::Inside,
); );
} }
@ -893,11 +851,6 @@ fn viewport_asset_drop_ui(
return; return;
} }
if surface_drag && over_viewport {
// The Last-schedule surface-drop session owns exact hit resolution and commit.
return;
}
if !pointer_pos.is_some_and(|pos| viewport_rect.contains(pos)) { if !pointer_pos.is_some_and(|pos| viewport_rect.contains(pos)) {
world.resource_mut::<EditorAssets>().clear_drag(); world.resource_mut::<EditorAssets>().clear_drag();
return; return;
@ -950,6 +903,26 @@ fn viewport_asset_drop_ui(
return; return;
} }
if let AssetSelection::SubAsset {
kind: AssetSubAssetKind::Texture,
source_path: Some(path),
label,
..
} = &selection
{
let asset = crate::assets::EditorAsset {
label: label.clone(),
path: Some(path.clone()),
folder_path: std::path::Path::new(path)
.parent()
.map(|parent| parent.to_string_lossy().replace('\\', "/"))
.unwrap_or_else(|| crate::assets::ASSETS_ROOT.to_string()),
kind: crate::assets::EditorAssetKind::Texture,
};
apply_texture_operator(world, asset, selected);
return;
}
let asset = { let asset = {
let assets = world.resource::<EditorAssets>(); let assets = world.resource::<EditorAssets>();
assets.asset_for_selection(&selection).cloned() assets.asset_for_selection(&selection).cloned()
@ -970,6 +943,18 @@ fn viewport_asset_drop_ui(
} }
return; return;
} }
match asset.kind {
crate::assets::EditorAssetKind::Texture => {
apply_texture_operator(world, asset, selected);
return;
}
crate::assets::EditorAssetKind::Material => {
apply_material_operator(world, asset, selected);
return;
}
_ => {}
}
place_asset_operator(world, asset, placement); place_asset_operator(world, asset, placement);
} }
@ -1011,11 +996,11 @@ fn asset_drag_descriptor(
AssetSelection::SubAsset { AssetSelection::SubAsset {
kind: AssetSubAssetKind::Texture, kind: AssetSubAssetKind::Texture,
.. ..
} => "Drop onto a primitive or brush face", } if !selected.is_empty() => "Apply texture to selection",
AssetSelection::SubAsset { AssetSelection::SubAsset {
kind: AssetSubAssetKind::Material, kind: AssetSubAssetKind::Material,
.. ..
} => "Drop onto an exact renderer slot", } if !selected.is_empty() => "Apply material to selection",
AssetSelection::SubAsset { AssetSelection::SubAsset {
kind: AssetSubAssetKind::Skeleton, kind: AssetSubAssetKind::Skeleton,
.. ..
@ -1035,8 +1020,12 @@ fn asset_drag_descriptor(
.. ..
} => "Create animated actor", } => "Create animated actor",
_ => match asset.kind { _ => match asset.kind {
crate::assets::EditorAssetKind::Texture => "Drop onto a primitive or brush face", crate::assets::EditorAssetKind::Texture if !selected.is_empty() => {
crate::assets::EditorAssetKind::Material => "Drop onto a renderable surface", "Apply texture to selection"
}
crate::assets::EditorAssetKind::Material if !selected.is_empty() => {
"Apply material to selection"
}
crate::assets::EditorAssetKind::AudioClip crate::assets::EditorAssetKind::AudioClip
if selected if selected
.as_slice() .as_slice()
@ -1052,29 +1041,6 @@ fn asset_drag_descriptor(
Some((icon, label, action.to_string())) Some((icon, label, action.to_string()))
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AssetDragVisualState {
Outside,
Targeted,
Valid,
Invalid,
}
impl AssetDragVisualState {
fn over_viewport(self) -> bool {
self != Self::Outside
}
fn color(self) -> egui::Color32 {
match self {
Self::Outside => TEXT_DIM,
Self::Targeted => ACCENT_HOVER,
Self::Valid => SUCCESS,
Self::Invalid => ERROR,
}
}
}
fn draw_asset_drag_preview( fn draw_asset_drag_preview(
ui: &egui::Ui, ui: &egui::Ui,
viewport_rect: egui::Rect, viewport_rect: egui::Rect,
@ -1082,41 +1048,39 @@ fn draw_asset_drag_preview(
icon: egui_phosphor_icons::Icon, icon: egui_phosphor_icons::Icon,
label: &str, label: &str,
action: &str, action: &str,
visual_state: AssetDragVisualState, over_viewport: bool,
) { ) {
let over_viewport = visual_state.over_viewport();
let highlight_color = visual_state.color();
if over_viewport { if over_viewport {
let painter = ui.painter(); let painter = ui.painter();
painter.circle_filled(pointer, 3.0, highlight_color); painter.circle_filled(pointer, 3.0, ACCENT_HOVER);
painter.circle_stroke(pointer, 11.0, egui::Stroke::new(1.5, highlight_color)); painter.circle_stroke(pointer, 11.0, egui::Stroke::new(1.5, ACCENT_HOVER));
painter.line_segment( painter.line_segment(
[ [
pointer - egui::vec2(17.0, 0.0), pointer - egui::vec2(17.0, 0.0),
pointer - egui::vec2(7.0, 0.0), pointer - egui::vec2(7.0, 0.0),
], ],
egui::Stroke::new(1.5, highlight_color), egui::Stroke::new(1.5, ACCENT_HOVER),
); );
painter.line_segment( painter.line_segment(
[ [
pointer + egui::vec2(7.0, 0.0), pointer + egui::vec2(7.0, 0.0),
pointer + egui::vec2(17.0, 0.0), pointer + egui::vec2(17.0, 0.0),
], ],
egui::Stroke::new(1.5, highlight_color), egui::Stroke::new(1.5, ACCENT_HOVER),
); );
painter.line_segment( painter.line_segment(
[ [
pointer - egui::vec2(0.0, 17.0), pointer - egui::vec2(0.0, 17.0),
pointer - egui::vec2(0.0, 7.0), pointer - egui::vec2(0.0, 7.0),
], ],
egui::Stroke::new(1.5, highlight_color), egui::Stroke::new(1.5, ACCENT_HOVER),
); );
painter.line_segment( painter.line_segment(
[ [
pointer + egui::vec2(0.0, 7.0), pointer + egui::vec2(0.0, 7.0),
pointer + egui::vec2(0.0, 17.0), pointer + egui::vec2(0.0, 17.0),
], ],
egui::Stroke::new(1.5, highlight_color), egui::Stroke::new(1.5, ACCENT_HOVER),
); );
} }
@ -1137,7 +1101,7 @@ fn draw_asset_drag_preview(
ui.set_width(228.0); ui.set_width(228.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(phosphor_icon(icon, 20.0).color(if over_viewport { ui.label(phosphor_icon(icon, 20.0).color(if over_viewport {
highlight_color ACCENT_HOVER
} else { } else {
TEXT_DIM TEXT_DIM
})); }));
@ -1152,7 +1116,7 @@ fn draw_asset_drag_preview(
}) })
.size(11.0) .size(11.0)
.color(if over_viewport { .color(if over_viewport {
highlight_color ACCENT_HOVER
} else { } else {
TEXT_MUTED TEXT_MUTED
}), }),
@ -1188,9 +1152,9 @@ fn draw_asset_drag_preview(
), ),
] { ] {
ui.painter() ui.painter()
.line_segment([a, a + b], egui::Stroke::new(3.0, highlight_color)); .line_segment([a, a + b], egui::Stroke::new(3.0, ACCENT_HOVER));
ui.painter() ui.painter()
.line_segment([a, a + c], egui::Stroke::new(3.0, highlight_color)); .line_segment([a, a + c], egui::Stroke::new(3.0, ACCENT_HOVER));
} }
} }
} }

View File

@ -1047,17 +1047,6 @@ fn pick_face(
best best
} }
pub(crate) fn pick_brush_face_id(
brush: &BrushDesc,
transform: &GlobalTransform,
ray: Ray3d,
) -> Option<(ComponentInstanceId, f32)> {
let (BrushElementKey::Face { face }, distance) = pick_face(brush, transform, ray)? else {
return None;
};
Some((face, distance))
}
fn world_vertices(face: &BrushFaceDesc, transform: &GlobalTransform) -> Vec<Vec3> { fn world_vertices(face: &BrushFaceDesc, transform: &GlobalTransform) -> Vec<Vec3> {
face.vertices face.vertices
.iter() .iter()

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@ pub mod brush_edit;
pub mod brush_tool; pub mod brush_tool;
pub mod camera; pub mod camera;
pub mod gizmos; pub mod gizmos;
pub mod material_drop;
mod panel; mod panel;
pub mod render_view; pub mod render_view;
pub mod rendering_diagnostics; pub mod rendering_diagnostics;
@ -22,6 +21,5 @@ pub use brush_csg::{
}; };
pub use brush_edit::{BrushEditMode, BrushEditPlugin, BrushElementSelection}; pub use brush_edit::{BrushEditMode, BrushEditPlugin, BrushElementSelection};
pub use brush_tool::{BrushToolPlugin, BrushToolState}; pub use brush_tool::{BrushToolPlugin, BrushToolState};
pub use material_drop::{MaterialDropFeedback, MaterialDropPlugin, MaterialDropState};
pub use panel::*; pub use panel::*;
pub use viewport_mode::EditorViewportMode; pub use viewport_mode::EditorViewportMode;

View File

@ -21,7 +21,6 @@ use crate::viewport::ViewportDisplayMode;
use crate::visualizers::EditorVisualizerProxy; use crate::visualizers::EditorVisualizerProxy;
use bevy_egui::egui; use bevy_egui::egui;
use crate::viewport::material_drop::{MaterialDropSet, MaterialDropState};
use crate::viewport::scene_view_ray; use crate::viewport::scene_view_ray;
#[derive(Resource, Default, Debug, Clone, Copy)] #[derive(Resource, Default, Debug, Clone, Copy)]
@ -48,7 +47,6 @@ struct PickTargetQueries<'w, 's> {
icon_proxies: Query<'w, 's, &'static ActorIconProxy>, icon_proxies: Query<'w, 's, &'static ActorIconProxy>,
parents: Query<'w, 's, &'static ChildOf>, parents: Query<'w, 's, &'static ChildOf>,
editor_only: Query<'w, 's, (), With<EditorOnly>>, editor_only: Query<'w, 's, (), With<EditorOnly>>,
material_drop: Option<Res<'w, MaterialDropState>>,
} }
impl Plugin for EditorSelectionPlugin { impl Plugin for EditorSelectionPlugin {
@ -70,7 +68,6 @@ impl Plugin for EditorSelectionPlugin {
Last, Last,
(handle_pick_events, cycle_overlapping_viewport_pick) (handle_pick_events, cycle_overlapping_viewport_pick)
.chain() .chain()
.after(MaterialDropSet)
.run_if(scene_tools_active), .run_if(scene_tools_active),
); );
} }
@ -113,14 +110,6 @@ fn handle_pick_events(
gizmo_targets: Query<&GizmoTarget>, gizmo_targets: Query<&GizmoTarget>,
hierarchy: Option<Res<HierarchyPanelState>>, hierarchy: Option<Res<HierarchyPanelState>>,
) -> Result { ) -> Result {
if pick_targets
.material_drop
.as_deref()
.is_some_and(MaterialDropState::captures_viewport_input)
{
viewport_click.0 = None;
return Ok(());
}
if ui_state.pointer_in_viewport if ui_state.pointer_in_viewport
&& !buttons.any_pressed([MouseButton::Right, MouseButton::Middle]) && !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
&& keys.just_pressed(KeyCode::Escape) && keys.just_pressed(KeyCode::Escape)
@ -227,14 +216,7 @@ fn cycle_overlapping_viewport_pick(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
buttons: Res<ButtonInput<MouseButton>>, buttons: Res<ButtonInput<MouseButton>>,
display: Res<ViewportDisplayMode>, display: Res<ViewportDisplayMode>,
material_drop: Option<Res<MaterialDropState>>,
) -> Result { ) -> Result {
if material_drop
.as_deref()
.is_some_and(MaterialDropState::captures_viewport_input)
{
return Ok(());
}
if display.clean_game_view { if display.clean_game_view {
return Ok(()); return Ok(());
} }

View File

@ -11,7 +11,8 @@ use bevy::prelude::*;
use crate::{ use crate::{
asset_server_path, authoring_component_active, brush_math::validate_brush, asset_server_path, authoring_component_active, brush_math::validate_brush,
AuthoringComponentStates, BrushDesc, BrushFaceDesc, ColliderDesc, InspectorOrder, LevelObject, AuthoringComponentStates, BrushDesc, BrushFaceDesc, ColliderDesc, InspectorOrder, LevelObject,
MaterialDesc, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_MATERIAL_DESC, MaterialAsset, MaterialDesc, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC,
COMPONENT_MATERIAL_DESC,
}; };
use super::materials::material_from_desc; use super::materials::material_from_desc;
@ -213,8 +214,8 @@ fn brush_group_material(
texture_path: Option<&str>, texture_path: Option<&str>,
) -> StandardMaterial { ) -> StandardMaterial {
let mut material = material_path let mut material = material_path
.and_then(|path| crate::load_resolved_material_from_path(path).ok()) .and_then(|path| MaterialAsset::load_from_path(path).ok())
.map(|(desc, _)| material_from_desc(asset_server, &desc)) .map(|asset| material_from_desc(asset_server, &asset.material))
.or_else(|| parent_material.map(|material| material_from_desc(asset_server, material))) .or_else(|| parent_material.map(|material| material_from_desc(asset_server, material)))
.unwrap_or_else(default_brush_material); .unwrap_or_else(default_brush_material);
if let Some(texture_path) = texture_path { if let Some(texture_path) = texture_path {

View File

@ -52,22 +52,12 @@ use crate::{
/// Registers hydration systems in deterministic order. /// Registers hydration systems in deterministic order.
pub struct HydrationPlugin; pub struct HydrationPlugin;
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum HydrationSet {
Visibility,
Content,
}
impl Plugin for HydrationPlugin { impl Plugin for HydrationPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<StaticMeshArtifactCache>() app.init_resource::<StaticMeshArtifactCache>()
.add_observer(tag_hydrated_prefab_members) .add_observer(tag_hydrated_prefab_members)
.add_observer(mark_skinned_materials_ready) .add_observer(mark_skinned_materials_ready)
.add_observer(crate::prefab_overrides::apply_prefab_overrides_on_ready) .add_observer(crate::prefab_overrides::apply_prefab_overrides_on_ready)
.configure_sets(
Update,
(HydrationSet::Visibility, HydrationSet::Content).chain(),
)
.add_systems( .add_systems(
Update, Update,
( (
@ -84,8 +74,7 @@ impl Plugin for HydrationPlugin {
hydrate_prefabs, hydrate_prefabs,
hydrate_physics, hydrate_physics,
) )
.chain() .chain(),
.in_set(HydrationSet::Content),
) )
.add_systems( .add_systems(
Update, Update,
@ -94,8 +83,7 @@ impl Plugin for HydrationPlugin {
sync_editor_visibility, sync_editor_visibility,
init_editor_visibility_on_spawn, init_editor_visibility_on_spawn,
) )
.chain() .chain(),
.in_set(HydrationSet::Visibility),
); );
} }
} }

View File

@ -10,9 +10,10 @@ use std::collections::{HashMap, HashSet};
use crate::{ use crate::{
asset_server_path, authoring_component_active, standard_material_asset_path, asset_server_path, authoring_component_active, standard_material_asset_path,
AuthoringComponentStates, ColliderDesc, ColliderShapeDesc, EditorAssetRef, AuthoringComponentStates, ColliderDesc, ColliderShapeDesc, EditorAssetRef,
HydratedRendererMaterialBinding, InspectorOrder, LevelObject, MaterialAlphaMode, MaterialDesc, HydratedRendererMaterialBinding, InspectorOrder, LevelObject, MaterialAlphaMode, MaterialAsset,
MaterialOverride, MaterialRef, StaticMeshRenderer, StaticMeshRendererEntry, MaterialDesc, MaterialInstanceAsset, MaterialOverride, MaterialRef, StaticMeshRenderer,
COMPONENT_COLLIDER_DESC, COMPONENT_MATERIAL_DESC, COMPONENT_STATIC_MESH_RENDERER, StaticMeshRendererEntry, COMPONENT_COLLIDER_DESC, COMPONENT_MATERIAL_DESC,
COMPONENT_STATIC_MESH_RENDERER,
}; };
use super::materials::material_from_desc; use super::materials::material_from_desc;
@ -456,7 +457,22 @@ fn resolve_shared_material(
return Some(handle.clone()); return Some(handle.clone());
} }
let path = reference.0.source_path.as_deref()?; let path = reference.0.source_path.as_deref()?;
let desc_and_state = crate::load_resolved_material_from_path(path).ok()?; let desc_and_state = MaterialAsset::load_from_path(path)
.map(|asset| (asset.material, asset.render_state))
.or_else(|_| {
let instance = MaterialInstanceAsset::load_from_path(path)?;
let base_path = instance.base.0.source_path.as_deref().ok_or_else(|| {
format!(
"material instance {} has no loadable base path",
instance.label
)
})?;
let base = MaterialAsset::load_from_path(base_path)?;
let mut desc = base.material;
instance.apply_to(&mut desc);
Ok::<_, String>((desc, base.render_state))
})
.ok()?;
let mut material = material_from_desc(asset_server, &desc_and_state.0); let mut material = material_from_desc(asset_server, &desc_and_state.0);
material.alpha_mode = match desc_and_state.1.alpha_mode { material.alpha_mode = match desc_and_state.1.alpha_mode {
MaterialAlphaMode::Opaque => AlphaMode::Opaque, MaterialAlphaMode::Opaque => AlphaMode::Opaque,

View File

@ -25,9 +25,9 @@ pub use hydration::{
HydratedPrefabReady, HydratedSkinnedMeshRoot, HydrationPlugin, PrefabHydrationBlocked, HydratedPrefabReady, HydratedSkinnedMeshRoot, HydrationPlugin, PrefabHydrationBlocked,
}; };
pub use material_asset::{ pub use material_asset::{
load_resolved_material_from_path, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset, MaterialAlphaMode, MaterialAsset, MaterialInstanceAsset, MaterialRenderState,
MaterialRenderState, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset, MATERIAL_ASSET_SCHEMA_VERSION,
MATERIAL_ASSET_SCHEMA_VERSION, MATERIAL_INSTANCE_SCHEMA_VERSION, SURFACE_SHADER_SCHEMA_VERSION, MATERIAL_INSTANCE_SCHEMA_VERSION, SURFACE_SHADER_SCHEMA_VERSION,
}; };
pub use navigation::*; pub use navigation::*;
pub use post_process_effect_asset::{PostProcessEffectAsset, PostProcessEffectKind}; pub use post_process_effect_asset::{PostProcessEffectAsset, PostProcessEffectKind};

View File

@ -79,33 +79,6 @@ impl MaterialAsset {
} }
} }
/// Loads a project Material or direct-base Material Instance into one effective descriptor.
///
/// The returned descriptor retains the selected asset path so primitive/brush authoring can locate
/// the shared source while renderer slots continue to preserve their stable [`MaterialRef`].
pub fn load_resolved_material_from_path(
catalog_path: &str,
) -> Result<(MaterialDesc, MaterialRenderState), String> {
if let Ok(asset) = MaterialAsset::load_from_path(catalog_path) {
let mut material = asset.material;
material.material_asset_path = Some(catalog_path.to_string());
return Ok((material, asset.render_state));
}
let instance = MaterialInstanceAsset::load_from_path(catalog_path)?;
let base_path = instance.base.0.source_path.as_deref().ok_or_else(|| {
format!(
"material instance {} has no loadable base path",
instance.label
)
})?;
let base = MaterialAsset::load_from_path(base_path)?;
let mut material = base.material;
instance.apply_to(&mut material);
material.material_asset_path = Some(catalog_path.to_string());
Ok((material, base.render_state))
}
/// Explicit reusable overrides over one project Material asset. /// Explicit reusable overrides over one project Material asset.
#[derive(Asset, TypePath, Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Asset, TypePath, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MaterialInstanceAsset { pub struct MaterialInstanceAsset {
@ -297,66 +270,4 @@ mod tests {
.iter() .iter()
.any(|value| value.name == "edge_width")); .any(|value| value.name == "edge_width"));
} }
#[test]
fn resolved_material_instance_applies_sparse_overrides_and_keeps_instance_path() {
let root = std::env::temp_dir().join(format!(
"blacksite-resolved-material-instance-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let base_path = root.join("base.ron");
let instance_path = root.join("instance.ron");
std::fs::create_dir_all(&root).unwrap();
let base = MaterialAsset {
schema_version: MATERIAL_ASSET_SCHEMA_VERSION,
label: "Base".into(),
shader: None,
shader_ref: None,
render_state: MaterialRenderState {
double_sided: true,
..Default::default()
},
material: MaterialDesc {
roughness: 0.8,
..Default::default()
},
};
std::fs::write(
&base_path,
ron::ser::to_string_pretty(&base, ron::ser::PrettyConfig::default()).unwrap(),
)
.unwrap();
let instance = MaterialInstanceAsset {
schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
label: "Instance".into(),
base: MaterialRef::new(
EditorAssetRef::new("base", "material:source", "Base")
.with_source_path(base_path.to_string_lossy().into_owned()),
),
parameters: vec![MaterialParameter {
name: "roughness".into(),
value: crate::MaterialParameterValue::Float(0.25),
}],
textures: Vec::new(),
};
std::fs::write(
&instance_path,
ron::ser::to_string_pretty(&instance, ron::ser::PrettyConfig::default()).unwrap(),
)
.unwrap();
let (resolved, state) =
load_resolved_material_from_path(instance_path.to_str().unwrap()).unwrap();
assert_eq!(resolved.roughness, 0.25);
assert_eq!(
resolved.material_asset_path.as_deref(),
instance_path.to_str()
);
assert!(state.double_sided);
std::fs::remove_dir_all(root).unwrap();
}
} }

View File

@ -25,7 +25,6 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
| [material-system.md](material-system.md) | Shared Material/Material Instance assets, static/skinned renderer slots, Surface ABI, Solari scope, and migration | | [material-system.md](material-system.md) | Shared Material/Material Instance assets, static/skinned renderer slots, Surface ABI, Solari scope, and migration |
| [collaborative-file-safety.md](collaborative-file-safety.md) | Exact authored-file revisions, Git/read-only status, conflict recovery, and optional ownership providers | | [collaborative-file-safety.md](collaborative-file-safety.md) | Exact authored-file revisions, Git/read-only status, conflict recovery, and optional ownership providers |
| [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation | | [evaluations/material-renderer-foundation/](evaluations/material-renderer-foundation/) | Live screenshots and verification record for the renderer/material/component foundation |
| [evaluations/material-library-targeted-drop/](evaluations/material-library-targeted-drop/) | Live screenshot and verification record for the docked Material Library and exact reversible surface drops |
| [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity | | [evaluations/navigation-authoring/](evaluations/navigation-authoring/) | Live screenshots and verification record for navigation authoring and runtime parity |
| [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence | | [evaluations/production-readiness/](evaluations/production-readiness/) | Current #50 release-candidate matrix and required clean-checkout, soak, performance, limitation, and independent-signoff evidence |
@ -41,7 +40,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
| `play/audio_preview.rs` / `game::audio` | Non-dirty editor audition and authored runtime playback/bus adapter | audio-authoring.md, ADR 0030 | | `play/audio_preview.rs` / `game::audio` | Non-dirty editor audition and authored runtime playback/bus adapter | audio-authoring.md, ADR 0030 |
| `assets/animation.rs` / `ui/animation_inspector.rs` / `game::animation` | Generated animation manifests, controller authoring/preview, and runtime state adapter | animation-authoring.md, ADR 0031 | | `assets/animation.rs` / `ui/animation_inspector.rs` / `game::animation` | Generated animation manifests, controller authoring/preview, and runtime state adapter | animation-authoring.md, ADR 0031 |
| `ui/navigation_inspector.rs` / `scene::navigation` / `game::navigation` | Navigation authoring, deterministic bake artifacts, viewport preview, and runtime path queries | navigation-authoring.md, ADR 0032 | | `ui/navigation_inspector.rs` / `scene::navigation` / `game::navigation` | Navigation authoring, deterministic bake artifacts, viewport preview, and runtime path queries | navigation-authoring.md, ADR 0032 |
| `assets/materials.rs` / `ui/material_library.rs` / `viewport/material_drop.rs` / `shared::renderer_material` | Shared material discovery, docked catalog/usage UI, exact reversible surface drops, stable renderer slots, and draw binding | material-system.md, ADR 0035 | | `assets/materials.rs` / `shared::renderer_material` / `shared::hydration::{static_meshes,skinned_meshes}` | Shared material discovery, stable renderer slots, and draw binding | material-system.md, ADR 0035 |
| `blacksite_surface` / `game_hot::rendering::solari` / `third_party/bevy_solari` | Surface ABI packing, raster composition, Solari evaluator dispatch, and deformation eligibility | material-system.md, rendering.md, ADR 0036 | | `blacksite_surface` / `game_hot::rendering::solari` / `third_party/bevy_solari` | Surface ABI packing, raster composition, Solari evaluator dispatch, and deformation eligibility | material-system.md, rendering.md, ADR 0036 |
| `assets/` | Catalog, asset DB, static mesh artifacts, `thumbnails/`, `materials.rs`, prefab overrides v2 | this file (below), prefab-authoring.md, ADR 0017, ADR 0027 | | `assets/` | Catalog, asset DB, static mesh artifacts, `thumbnails/`, `materials.rs`, prefab overrides v2 | this file (below), prefab-authoring.md, ADR 0017, ADR 0027 |
| `shared::prefab_overrides` | Versioned stable override schema and editor-independent runtime application | prefab-authoring.md, ADR 0027 | | `shared::prefab_overrides` | Versioned stable override schema and editor-independent runtime application | prefab-authoring.md, ADR 0027 |
@ -69,7 +68,6 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
- **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping. - **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping.
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera. - **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
- **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. **Audio** accepts Bevy-supported Ogg/Vorbis and Speex (`.ogg`, `.oga`, `.spx`), WAV, MP3, and FLAC clips under `assets/audio/`, with a dedicated filter, waveform icon, format/file details, and stable registry-backed references that retain a runtime source path. 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/`. Before thumbnail loading, glTF sources preflight local external buffers/images and show a stable non-retryable failure state when a dependency is missing instead of repeatedly invoking the asset loader. - **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. **Audio** accepts Bevy-supported Ogg/Vorbis and Speex (`.ogg`, `.oga`, `.spx`), WAV, MP3, and FLAC clips under `assets/audio/`, with a dedicated filter, waveform icon, format/file details, and stable registry-backed references that retain a runtime source path. 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/`. Before thumbnail loading, glTF sources preflight local external buffers/images and show a stable non-retryable failure state when a dependency is missing instead of repeatedly invoking the asset loader.
- **Material Library** is a dockable bottom-panel catalog for cross-folder Material and direct-base Material Instance authoring. It provides search, type and scene-usage filters, grid/list thumbnails, dependency health, usage counts, creation, guarded details editing, and first-class drag sources. Viewport drops resolve an exact renderer slot, primitive, or brush face under the pointer, preview transiently, reject incompatible/read-only targets explicitly, restore on target change/cancel, and commit one typed undo step on release.
- **Static/skinned renderer split** — model drag/drop uses normalized artifacts under `assets/meshes/generated/`. Unrigged, non-animated sources create `ActorKind::StaticMesh + StaticMeshRenderer`; skin-bound or animated sources create `ActorKind::SkinnedMesh + SkinnedMeshRenderer` and preserve the imported hierarchy. Static slots never contain marked skinned primitives or geometry from animated sources. `SceneInstance` placement keeps `ImportedModel + ModelRef` for generic full-source scenes. See [ADR 0033](../adr/0033-dedicated-skinned-mesh-renderer.md). - **Static/skinned renderer split** — model drag/drop uses normalized artifacts under `assets/meshes/generated/`. Unrigged, non-animated sources create `ActorKind::StaticMesh + StaticMeshRenderer`; skin-bound or animated sources create `ActorKind::SkinnedMesh + SkinnedMeshRenderer` and preserve the imported hierarchy. Static slots never contain marked skinned primitives or geometry from animated sources. `SceneInstance` placement keeps `ImportedModel + ModelRef` for generic full-source scenes. See [ADR 0033](../adr/0033-dedicated-skinned-mesh-renderer.md).
- **Animation authoring** — glTF/GLB imports generate UUID-keyed rig/clip manifests and expandable skeleton/clip shelves. Clip drag creates a skinned actor or adds one exact-signature-compatible state to a selected skinned actor through typed history. `AnimationControllerDesc` exposes named states, ranges, loop/speed, default/crossfade, and non-dirty runtime-backed preview; project validation resolves the same stable refs and blocks animated/skinned FBX with conversion guidance. Production acceptance completed in Gitea #46. See [animation-authoring.md](animation-authoring.md) and [ADR 0031](../adr/0031-animation-authoring-runtime-contract.md). - **Animation authoring** — glTF/GLB imports generate UUID-keyed rig/clip manifests and expandable skeleton/clip shelves. Clip drag creates a skinned actor or adds one exact-signature-compatible state to a selected skinned actor through typed history. `AnimationControllerDesc` exposes named states, ranges, loop/speed, default/crossfade, and non-dirty runtime-backed preview; project validation resolves the same stable refs and blocks animated/skinned FBX with conversion guidance. Production acceptance completed in Gitea #46. See [animation-authoring.md](animation-authoring.md) and [ADR 0031](../adr/0031-animation-authoring-runtime-contract.md).
- **Navigation authoring** — bounds, obstacles, areas, and links use typed history and viewport visualizers. Rerecast produces a versioned fingerprinted artifact; Polyanya powers both editor path preview and the game-owned runtime API. The existing horizontal toolbar path button creates or bakes bounds, while Scene > Navigation exposes the complete workflow. See [navigation-authoring.md](navigation-authoring.md) and [ADR 0032](../adr/0032-versioned-navigation-bake-and-runtime-query.md). - **Navigation authoring** — bounds, obstacles, areas, and links use typed history and viewport visualizers. Rerecast produces a versioned fingerprinted artifact; Polyanya powers both editor path preview and the game-owned runtime API. The existing horizontal toolbar path button creates or bakes bounds, while Scene > Navigation exposes the complete workflow. See [navigation-authoring.md](navigation-authoring.md) and [ADR 0032](../adr/0032-versioned-navigation-bake-and-runtime-query.md).

View File

@ -1,47 +0,0 @@
# Material Library And Targeted Drop Evaluation
Date: 2026-07-12
Branch: `codex/material-library-targeted-drop`
Implementation commit: `02247f0`
This record captures live acceptance for the docked Material Library and exact viewport
Material/Texture assignment workflow. The permanent user workflow lives in the
[material-system guide](../../material-system.md), and the implementation plan is
[material_library_and_targeted_drop_2026-07-12.plan.md](../../../../.cursor/plans/material_library_and_targeted_drop_2026-07-12.plan.md).
## Live Editor Evidence
The image below is a native Wayland capture from the debug editor. The assignment was previewed,
committed once, undone, previewed again, and canceled with Escape; the authored scene was not saved.
![Material Instance preview on the exact Ground surface with the docked Material Library and green target feedback](material-instance-surface-preview.png)
The Material Library resolves `surface_tint_instance` through its base Material and sparse
overrides. While the pointer is over the viewport, the green card names `Ground / Surface`, the
inspector reflects the transient material, and the scene-usage section identifies the exact actor
and surface. The preview does not push history or dirty the scene.
## Acceptance Results
| Area | Result | Evidence |
|------|--------|----------|
| Catalog workflow | Pass | Window menu entry, persisted bottom dock, search, Material/Instance/Used/Unused filters, grid/list views, resolved instance thumbnail, and details selection |
| Exact targeting | Pass | Primitive preview resolves one actor surface; automated coverage resolves one stable renderer slot and one stable brush face |
| Feedback | Pass | Neutral acquisition changes to green valid actor/surface feedback; invalid payload and read-only paths are covered by focused tests |
| Preview lifecycle | Pass | Live material and usage update without a history entry; Escape restores the exact prior material |
| Commit lifecycle | Pass | Release creates one typed history/operator entry; undo restores the prior surface |
| Startup hygiene | Pass | Relaunching the authored showcase after visibility-order correction produced no Bevy hierarchy warnings |
| Packaged acceptance | Deferred | Explicitly deferred by project-owner direction; no packaged result is claimed here |
## Automated Verification
| Command/suite | Result |
|---------------|--------|
| `cargo fmt --all -- --check` | Pass |
| `cargo check --workspace --all-targets` | Pass |
| `cargo clippy --workspace --all-targets -- -D warnings` | Pass |
| `cargo test --workspace` | Pass |
| Editor strict Clippy with `dev` and `hot-reload` features | Pass |
| `cargo validate-levels --project .` | 59 dependencies, 5 non-blocking findings, 0 blocking errors |
| `cargo bake-navigation --project . --check` | 1 deterministic artifact current |
| `git diff --check` | Pass |

View File

@ -21,34 +21,17 @@ material, but may not reference another instance.
## Assign materials ## Assign materials
Open **Window > Material Library** for the cross-folder Material/Material Instance catalog. It 1. Select a static- or skinned-mesh actor.
supports search, Material/Instance/scene-usage filters, grid/list thumbnails, dependency health, 2. In **Static Mesh Renderer** or **Skinned Mesh Renderer**, find the material slot by its imported
scene-reference counts, creation, drag sources, and the guarded shared-asset editor. It lives in the name and stable ID.
existing bottom dock and does not add permanent viewport chrome. Scene-usage rows select the exact 3. Use **Browse** or **Select** to assign a Material or Material Instance. **Locate** reveals the
actor, focus the viewport, and identify its renderer slot, primitive surface, or brush face. current asset in the Asset Browser.
4. Use **Clear** to remove only the explicit assignment. The slot returns to its read-only imported
Assign through either exact workflow: source material.
1. Drag a Material or Material Instance from the Material Library or Asset Browser onto the desired
viewport surface. A hydrated static/skinned draw targets its exact stable renderer slot, a
primitive targets its actor material, and a brush targets the exact face under the pointer.
2. Check the pointer card and target visual. Green names a valid actor/slot/face; red gives the
blocking reason. Release commits one undoable assignment. Move away, leave the viewport, press
Escape, or right-click to restore the pre-drag state without history or dirtying the scene.
3. Alternatively, select a static- or skinned-mesh actor and use **Browse** or **Select** on the
named slot in its renderer inspector. **Locate** reveals the current asset in the Asset Browser;
**Clear** removes only the explicit assignment and restores the read-only imported source.
From the Asset Browser, **Apply Material** assigns the selected material to every material slot on From the Asset Browser, **Apply Material** assigns the selected material to every material slot on
each selected static or skinned renderer. This is the explicit bulk path. A viewport hit never each selected static or skinned renderer. Use the per-slot inspector when different draws need
silently broadens to all slots. different assignments.
Texture drops target a primitive's base-color texture or one brush face. Direct Texture drops on a
renderer slot are rejected because a renderer slot stores a Material reference; create or edit a
Material Instance and assign the texture there.
See the [Material Library and targeted-drop evaluation](evaluations/material-library-targeted-drop/)
for the live preview, commit/undo, cancel, and source-verification record.
Static draw slots and material slots are separate. Removing a static draw retains its explicit Static draw slots and material slots are separate. Removing a static draw retains its explicit
material assignment as an orphan rather than guessing a replacement. Reimport also reconciles material assignment as an orphan rather than guessing a replacement. Reimport also reconciles
@ -56,11 +39,10 @@ slots by stable ID, never by display name. Resolve an orphan explicitly in the r
## Author shared assets ## Author shared assets
Material files and material-instance files are RON documents under `assets/materials/`. The Material files and material-instance files are RON documents under `assets/materials/`. The Asset
Material Library and Asset Browser identify the document kind, expose the same guarded Browser identifies the document kind, exposes its schema-driven properties and texture bindings,
schema-driven editor and texture bindings, and use stable subasset IDs (`material:source` or and uses stable subasset IDs (`material:source` or `material:instance`). Built-in shader schemas
`material:instance`). Material Instance thumbnails resolve the direct base plus sparse overrides. live under `assets/shaders/`.
Built-in shader schemas live under `assets/shaders/`.
Select a Material and use **Create Instance** to create a direct-base variant beside it. The Select a Material and use **Create Instance** to create a direct-base variant beside it. The
instance inspector exposes its base Material plus sparse property and texture override checkboxes; instance inspector exposes its base Material plus sparse property and texture override checkboxes;
@ -165,5 +147,3 @@ bases, and unsupported project content through the same editor/headless finding
| Custom normal has no visible effect | Verify the mesh has tangents. | | Custom normal has no visible effect | Verify the mesh has tangents. |
| Animated actor is missing from Solari GI | Expected for current skinned/morph geometry; inspect the deformed-exclusion diagnostic. | | Animated actor is missing from Solari GI | Expected for current skinned/morph geometry; inspect the deformed-exclusion diagnostic. |
| Material reference is unresolved | Run project validation and repair/reselect the stable asset reference; do not hand-edit a display-name match. | | Material reference is unresolved | Run project validation and repair/reselect the stable asset reference; do not hand-edit a display-name match. |
| Texture drop is red on an imported renderer | Renderer slots accept Materials, not loose textures. Create/edit a Material Instance, set its texture, then assign it. |
| Drag preview remains after leaving a target | This is a regression: target changes, viewport exit, Escape, right-click, and invalid release must restore the exact snapshot. |