Add brush face asset pickers
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
4dc22cd3b6
commit
015ae6da63
@ -42,7 +42,7 @@ use super::component_registry::{
|
|||||||
use super::dock_tabs::open_and_focus_tab;
|
use super::dock_tabs::open_and_focus_tab;
|
||||||
use super::helpers::create_scene_sun_override_from_project_settings;
|
use super::helpers::create_scene_sun_override_from_project_settings;
|
||||||
use super::{EditorTab, UiState};
|
use super::{EditorTab, UiState};
|
||||||
use crate::assets::asset_db::AssetRegistry;
|
use crate::assets::asset_db::{find_asset_by_path, AssetRegistry};
|
||||||
use crate::assets::static_mesh::{
|
use crate::assets::static_mesh::{
|
||||||
load_static_mesh_manifest, material_id_from_label, part_id_from_label,
|
load_static_mesh_manifest, material_id_from_label, part_id_from_label,
|
||||||
};
|
};
|
||||||
@ -91,6 +91,7 @@ struct AssetRefCandidate {
|
|||||||
enum AssetRefCandidateKind {
|
enum AssetRefCandidateKind {
|
||||||
Mesh,
|
Mesh,
|
||||||
Material,
|
Material,
|
||||||
|
Texture,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -98,6 +99,7 @@ struct AssetSelectorResponse {
|
|||||||
selected: Option<EditorAssetRef>,
|
selected: Option<EditorAssetRef>,
|
||||||
clear: bool,
|
clear: bool,
|
||||||
locate: bool,
|
locate: bool,
|
||||||
|
accepted_drop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -1335,6 +1337,7 @@ fn static_mesh_asset_ref_candidates(
|
|||||||
texture_id: None,
|
texture_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AssetRefCandidateKind::Texture => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = (
|
let key = (
|
||||||
@ -1380,6 +1383,200 @@ fn texture_asset_candidates(world: &World) -> Vec<TextureAssetCandidate> {
|
|||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn texture_asset_ref_candidates(world: &World) -> Vec<AssetRefCandidate> {
|
||||||
|
let Some(catalog) = world.get_resource::<EditorAssets>() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(registry) = world.get_resource::<AssetRegistry>() 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.as_deref()?;
|
||||||
|
let record = find_asset_by_path(registry, path)?;
|
||||||
|
Some(AssetRefCandidate {
|
||||||
|
reference: EditorAssetRef::new(
|
||||||
|
record.id.as_string(),
|
||||||
|
"texture:source",
|
||||||
|
asset.label.clone(),
|
||||||
|
),
|
||||||
|
label: asset.label.clone(),
|
||||||
|
detail: path.to_string(),
|
||||||
|
selection: AssetSelection::File(path.to_string()),
|
||||||
|
folder_path: asset.folder_path.clone(),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: snapshot
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|snapshot| snapshot.texture_for(asset)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.detail.cmp(&b.detail)));
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn brush_face_material_ref_candidates(world: &mut World) -> Vec<AssetRefCandidate> {
|
||||||
|
let mut candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Material);
|
||||||
|
let Some(catalog) = world.get_resource::<EditorAssets>() else {
|
||||||
|
return candidates;
|
||||||
|
};
|
||||||
|
let Some(registry) = world.get_resource::<AssetRegistry>() else {
|
||||||
|
return candidates;
|
||||||
|
};
|
||||||
|
let mut seen: HashSet<_> = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| {
|
||||||
|
(
|
||||||
|
candidate.reference.asset_id.clone(),
|
||||||
|
candidate.reference.sub_asset_id.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for asset in catalog
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.filter(|asset| matches!(asset.kind, EditorAssetKind::Material))
|
||||||
|
{
|
||||||
|
let Some(path) = asset.path.as_deref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(record) = find_asset_by_path(registry, path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let reference = EditorAssetRef::new(record.id.as_string(), "material:source", &asset.label);
|
||||||
|
let key = (reference.asset_id.clone(), reference.sub_asset_id.clone());
|
||||||
|
if !seen.insert(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
candidates.push(AssetRefCandidate {
|
||||||
|
reference,
|
||||||
|
label: asset.label.clone(),
|
||||||
|
detail: path.to_string(),
|
||||||
|
selection: AssetSelection::File(path.to_string()),
|
||||||
|
folder_path: asset.folder_path.clone(),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.detail.cmp(&b.detail)));
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asset_ref_candidate_from_selection(
|
||||||
|
world: &World,
|
||||||
|
selection: &AssetSelection,
|
||||||
|
kind: AssetRefCandidateKind,
|
||||||
|
) -> Option<AssetRefCandidate> {
|
||||||
|
let registry = world.get_resource::<AssetRegistry>()?;
|
||||||
|
match (kind, selection) {
|
||||||
|
(AssetRefCandidateKind::Material, AssetSelection::File(path)) => {
|
||||||
|
let asset = world
|
||||||
|
.get_resource::<EditorAssets>()?
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| {
|
||||||
|
matches!(asset.kind, EditorAssetKind::Material)
|
||||||
|
&& asset.path.as_deref() == Some(path.as_str())
|
||||||
|
})?;
|
||||||
|
let record = find_asset_by_path(registry, path)?;
|
||||||
|
Some(AssetRefCandidate {
|
||||||
|
reference: EditorAssetRef::new(
|
||||||
|
record.id.as_string(),
|
||||||
|
"material:source",
|
||||||
|
asset.label.clone(),
|
||||||
|
),
|
||||||
|
label: asset.label.clone(),
|
||||||
|
detail: path.clone(),
|
||||||
|
selection: selection.clone(),
|
||||||
|
folder_path: asset.folder_path.clone(),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(AssetRefCandidateKind::Texture, 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())
|
||||||
|
})?;
|
||||||
|
let record = find_asset_by_path(registry, path)?;
|
||||||
|
Some(AssetRefCandidate {
|
||||||
|
reference: EditorAssetRef::new(
|
||||||
|
record.id.as_string(),
|
||||||
|
"texture:source",
|
||||||
|
asset.label.clone(),
|
||||||
|
),
|
||||||
|
label: asset.label.clone(),
|
||||||
|
detail: path.clone(),
|
||||||
|
selection: selection.clone(),
|
||||||
|
folder_path: asset.folder_path.clone(),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
AssetRefCandidateKind::Material,
|
||||||
|
AssetSelection::SubAsset {
|
||||||
|
parent_path,
|
||||||
|
sub_asset_id,
|
||||||
|
label,
|
||||||
|
kind: AssetSubAssetKind::Material,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let record = find_asset_by_path(registry, parent_path)?;
|
||||||
|
Some(AssetRefCandidate {
|
||||||
|
reference: EditorAssetRef::new(
|
||||||
|
record.id.as_string(),
|
||||||
|
sub_asset_id.clone(),
|
||||||
|
label.clone(),
|
||||||
|
),
|
||||||
|
label: label.clone(),
|
||||||
|
detail: parent_path.clone(),
|
||||||
|
selection: selection.clone(),
|
||||||
|
folder_path: fallback_asset_folder(parent_path),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
AssetRefCandidateKind::Texture,
|
||||||
|
AssetSelection::SubAsset {
|
||||||
|
parent_path,
|
||||||
|
sub_asset_id,
|
||||||
|
label,
|
||||||
|
kind: AssetSubAssetKind::Texture,
|
||||||
|
source_path,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let record = find_asset_by_path(registry, parent_path)?;
|
||||||
|
Some(AssetRefCandidate {
|
||||||
|
reference: EditorAssetRef::new(
|
||||||
|
record.id.as_string(),
|
||||||
|
sub_asset_id.clone(),
|
||||||
|
label.clone(),
|
||||||
|
),
|
||||||
|
label: label.clone(),
|
||||||
|
detail: source_path.clone().unwrap_or_else(|| parent_path.clone()),
|
||||||
|
selection: selection.clone(),
|
||||||
|
folder_path: fallback_asset_folder(parent_path),
|
||||||
|
source_material: None,
|
||||||
|
texture_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn request_texture_asset_thumbnails(world: &mut World) {
|
fn request_texture_asset_thumbnails(world: &mut World) {
|
||||||
let requests: Vec<_> = world
|
let requests: Vec<_> = world
|
||||||
.get_resource::<EditorAssets>()
|
.get_resource::<EditorAssets>()
|
||||||
@ -1624,6 +1821,7 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
|||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
&mesh_candidates,
|
&mesh_candidates,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
if let Some(selected) = mesh_response.selected {
|
if let Some(selected) = mesh_response.selected {
|
||||||
entry.mesh = selected;
|
entry.mesh = selected;
|
||||||
@ -1641,6 +1839,7 @@ fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
|||||||
inherited_material.as_ref(),
|
inherited_material.as_ref(),
|
||||||
true,
|
true,
|
||||||
&material_candidates,
|
&material_candidates,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
if let Some(selected) = material_response.selected {
|
if let Some(selected) = material_response.selected {
|
||||||
entry.material = Some(selected);
|
entry.material = Some(selected);
|
||||||
@ -1830,6 +2029,7 @@ fn asset_selector_row(
|
|||||||
inherited_asset: Option<&EditorAssetRef>,
|
inherited_asset: Option<&EditorAssetRef>,
|
||||||
clearable: bool,
|
clearable: bool,
|
||||||
candidates: &[AssetRefCandidate],
|
candidates: &[AssetRefCandidate],
|
||||||
|
drop_candidate: Option<&AssetRefCandidate>,
|
||||||
) -> AssetSelectorResponse {
|
) -> AssetSelectorResponse {
|
||||||
let mut response = AssetSelectorResponse::default();
|
let mut response = AssetSelectorResponse::default();
|
||||||
if ui.available_width() < COMPACT_INSPECTOR_WIDTH {
|
if ui.available_width() < COMPACT_INSPECTOR_WIDTH {
|
||||||
@ -1844,6 +2044,7 @@ fn asset_selector_row(
|
|||||||
clearable,
|
clearable,
|
||||||
control_width,
|
control_width,
|
||||||
candidates,
|
candidates,
|
||||||
|
drop_candidate,
|
||||||
&mut response,
|
&mut response,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1861,6 +2062,7 @@ fn asset_selector_row(
|
|||||||
clearable,
|
clearable,
|
||||||
control_width,
|
control_width,
|
||||||
candidates,
|
candidates,
|
||||||
|
drop_candidate,
|
||||||
&mut response,
|
&mut response,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1876,6 +2078,7 @@ fn asset_selector_control(
|
|||||||
clearable: bool,
|
clearable: bool,
|
||||||
control_width: f32,
|
control_width: f32,
|
||||||
candidates: &[AssetRefCandidate],
|
candidates: &[AssetRefCandidate],
|
||||||
|
drop_candidate: Option<&AssetRefCandidate>,
|
||||||
response: &mut AssetSelectorResponse,
|
response: &mut AssetSelectorResponse,
|
||||||
) {
|
) {
|
||||||
let display_asset = asset.or(inherited_asset);
|
let display_asset = asset.or(inherited_asset);
|
||||||
@ -1887,10 +2090,25 @@ fn asset_selector_control(
|
|||||||
egui::Layout::top_down(egui::Align::Min),
|
egui::Layout::top_down(egui::Align::Min),
|
||||||
|ui| {
|
|ui| {
|
||||||
ui.set_clip_rect(ui.max_rect());
|
ui.set_clip_rect(ui.max_rect());
|
||||||
|
let rect = ui.max_rect();
|
||||||
|
let valid_drag = drop_candidate.is_some();
|
||||||
|
let drop_hovered = valid_drag && ui.rect_contains_pointer(rect);
|
||||||
|
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 {
|
||||||
|
egui::Stroke::new(1.0, BORDER)
|
||||||
|
};
|
||||||
|
let fill = if drop_hovered {
|
||||||
|
egui::Color32::from_rgb(29, 57, 86)
|
||||||
|
} else {
|
||||||
|
WIDGET_BG.linear_multiply(0.75)
|
||||||
|
};
|
||||||
let inner_width = (selector_width - 18.0).max(1.0);
|
let inner_width = (selector_width - 18.0).max(1.0);
|
||||||
egui::Frame::new()
|
egui::Frame::new()
|
||||||
.fill(WIDGET_BG.linear_multiply(0.75))
|
.fill(fill)
|
||||||
.stroke(egui::Stroke::new(1.0, BORDER))
|
.stroke(stroke)
|
||||||
.corner_radius(egui::CornerRadius::same(4))
|
.corner_radius(egui::CornerRadius::same(4))
|
||||||
.inner_margin(egui::Margin::symmetric(8, 6))
|
.inner_margin(egui::Margin::symmetric(8, 6))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@ -1981,6 +2199,12 @@ fn asset_selector_control(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if drop_hovered && ui.input(|input| input.pointer.any_released()) {
|
||||||
|
if let Some(candidate) = drop_candidate {
|
||||||
|
response.selected = Some(candidate.reference.clone());
|
||||||
|
response.accepted_drop = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2402,6 +2626,9 @@ pub fn material_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||||
|
let material_candidates = brush_face_material_ref_candidates(world);
|
||||||
|
request_texture_asset_thumbnails(world);
|
||||||
|
let texture_candidates = texture_asset_ref_candidates(world);
|
||||||
let Some(mut brush) = world.get::<BrushDesc>(entity).cloned() else {
|
let Some(mut brush) = world.get::<BrushDesc>(entity).cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -2443,7 +2670,14 @@ fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
brush_validation_ui(ui, &brush);
|
brush_validation_ui(ui, &brush);
|
||||||
changed |= selected_brush_face_controls(world, ui, entity, &mut brush);
|
changed |= selected_brush_face_controls(
|
||||||
|
world,
|
||||||
|
ui,
|
||||||
|
entity,
|
||||||
|
&mut brush,
|
||||||
|
&material_candidates,
|
||||||
|
&texture_candidates,
|
||||||
|
);
|
||||||
if ui.button("Reset Cube Brush").clicked() {
|
if ui.button("Reset Cube Brush").clicked() {
|
||||||
reset_cube = true;
|
reset_cube = true;
|
||||||
}
|
}
|
||||||
@ -2514,10 +2748,12 @@ fn brush_validation_ui(ui: &mut egui::Ui, brush: &BrushDesc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn selected_brush_face_controls(
|
fn selected_brush_face_controls(
|
||||||
world: &World,
|
world: &mut World,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
brush: &mut BrushDesc,
|
brush: &mut BrushDesc,
|
||||||
|
material_candidates: &[AssetRefCandidate],
|
||||||
|
texture_candidates: &[AssetRefCandidate],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let Some(selection) = world.get_resource::<BrushElementSelection>() else {
|
let Some(selection) = world.get_resource::<BrushElementSelection>() else {
|
||||||
return false;
|
return false;
|
||||||
@ -2537,6 +2773,15 @@ fn selected_brush_face_controls(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dragging_selection = world
|
||||||
|
.get_resource::<EditorAssets>()
|
||||||
|
.and_then(|assets| assets.dragging_selection().cloned());
|
||||||
|
let material_drop_candidate = dragging_selection.as_ref().and_then(|selection| {
|
||||||
|
asset_ref_candidate_from_selection(world, selection, AssetRefCandidateKind::Material)
|
||||||
|
});
|
||||||
|
let texture_drop_candidate = dragging_selection.as_ref().and_then(|selection| {
|
||||||
|
asset_ref_candidate_from_selection(world, selection, AssetRefCandidateKind::Texture)
|
||||||
|
});
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@ -2577,51 +2822,65 @@ fn selected_brush_face_controls(
|
|||||||
.add(egui::DragValue::new(&mut face.uv_rotation).speed(1.0))
|
.add(egui::DragValue::new(&mut face.uv_rotation).speed(1.0))
|
||||||
.changed();
|
.changed();
|
||||||
});
|
});
|
||||||
changed |= editor_asset_ref_option_ui(ui, "Material", &mut face.material);
|
let material_response = asset_selector_row(
|
||||||
changed |= editor_asset_ref_option_ui(ui, "Texture", &mut face.texture);
|
ui,
|
||||||
|
"Material",
|
||||||
|
icons::PALETTE,
|
||||||
|
face.material.as_ref(),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
material_candidates,
|
||||||
|
material_drop_candidate.as_ref(),
|
||||||
|
);
|
||||||
|
if let Some(selected) = material_response.selected {
|
||||||
|
face.material = Some(selected);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if material_response.clear {
|
||||||
|
face.material = None;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if material_response.locate {
|
||||||
|
locate_asset_ref(world, face.material.as_ref(), material_candidates);
|
||||||
|
}
|
||||||
|
if material_response.accepted_drop {
|
||||||
|
clear_asset_drag(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
let texture_response = asset_selector_row(
|
||||||
|
ui,
|
||||||
|
"Texture",
|
||||||
|
icons::IMAGE,
|
||||||
|
face.texture.as_ref(),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
texture_candidates,
|
||||||
|
texture_drop_candidate.as_ref(),
|
||||||
|
);
|
||||||
|
if let Some(selected) = texture_response.selected {
|
||||||
|
face.texture = Some(selected);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if texture_response.clear {
|
||||||
|
face.texture = None;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if texture_response.locate {
|
||||||
|
locate_asset_ref(world, face.texture.as_ref(), texture_candidates);
|
||||||
|
}
|
||||||
|
if texture_response.accepted_drop {
|
||||||
|
clear_asset_drag(world);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_asset_ref_option_ui(
|
fn clear_asset_drag(world: &mut World) {
|
||||||
ui: &mut egui::Ui,
|
if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
|
||||||
label: &str,
|
assets.clear_drag();
|
||||||
value: &mut Option<EditorAssetRef>,
|
}
|
||||||
) -> bool {
|
|
||||||
let mut changed = false;
|
|
||||||
property_row(ui, label, |ui| {
|
|
||||||
let mut enabled = value.is_some();
|
|
||||||
if ui.checkbox(&mut enabled, "").changed() {
|
|
||||||
if enabled {
|
|
||||||
*value = Some(EditorAssetRef::default());
|
|
||||||
} else {
|
|
||||||
*value = None;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if let Some(reference) = value.as_mut() {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_min_width(MIN_INLINE_CONTROL_WIDTH);
|
|
||||||
changed |= ui
|
|
||||||
.text_edit_singleline(&mut reference.label)
|
|
||||||
.on_hover_text("Cached display label")
|
|
||||||
.changed();
|
|
||||||
changed |= ui
|
|
||||||
.text_edit_singleline(&mut reference.asset_id)
|
|
||||||
.on_hover_text("Asset registry ID")
|
|
||||||
.changed();
|
|
||||||
changed |= ui
|
|
||||||
.text_edit_singleline(&mut reference.sub_asset_id)
|
|
||||||
.on_hover_text("Imported sub-asset ID")
|
|
||||||
.changed();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ui.label(egui::RichText::new("(none)").color(TEXT_DIM));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
changed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
||||||
@ -2914,6 +3173,7 @@ fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
|
|||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
&mesh_candidates,
|
&mesh_candidates,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
if let Some(selected) = mesh_response.selected {
|
if let Some(selected) = mesh_response.selected {
|
||||||
*mesh = selected;
|
*mesh = selected;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ With a brush actor selected, `1`/`2`/`3`/`4` enter vertex, edge, face, and clip
|
|||||||
|
|
||||||
Vertex, edge, and face selections can be dragged on the viewport floor plane. The editor moves matching duplicated corner vertices across all brush faces so simple cube/prism brushes stay welded, recomputes face planes, and commits one undoable brush edit when the drag releases. Clip mode currently provides element visualization/selection only; split application is future CSG work.
|
Vertex, edge, and face selections can be dragged on the viewport floor plane. The editor moves matching duplicated corner vertices across all brush faces so simple cube/prism brushes stay welded, recomputes face planes, and commits one undoable brush edit when the drag releases. Clip mode currently provides element visualization/selection only; split application is future CSG work.
|
||||||
|
|
||||||
When one or more faces are selected in Face mode, the Brush inspector shows selected-face controls for UV offset, UV scale, UV rotation, material ref, and texture ref. These fields edit the persisted `BrushFaceDesc` data through undoable brush updates. The current controls are data-oriented; Asset Browser picker/drop polish and per-face material batching in hydration are still follow-up work.
|
When one or more faces are selected in Face mode, the Brush inspector shows selected-face controls for UV offset, UV scale, UV rotation, material ref, and texture ref. Material and texture rows use Asset Browser-aware picker controls with clear, locate, browse, and compatible drag/drop assignment. These fields edit the persisted `BrushFaceDesc` data through undoable brush updates. Per-face material batching in hydration is still follow-up work.
|
||||||
|
|
||||||
## Boolean Operations
|
## Boolean Operations
|
||||||
|
|
||||||
@ -36,4 +36,4 @@ These operations commit through history but are not full arbitrary-face CSG yet.
|
|||||||
- Brush diagnostics currently focus on face validity, plane normals, finite vertices, duplicate vertices, degenerate area, and UV scale warnings; a dedicated multi-brush diagnostics window is future work.
|
- Brush diagnostics currently focus on face validity, plane normals, finite vertices, duplicate vertices, degenerate area, and UV scale warnings; a dedicated multi-brush diagnostics window is future work.
|
||||||
- Subtractive brush markers are stored but not automatically evaluated.
|
- Subtractive brush markers are stored but not automatically evaluated.
|
||||||
- Clip split application and arbitrary-face CSG remain future roadmap work.
|
- Clip split application and arbitrary-face CSG remain future roadmap work.
|
||||||
- Per-face material and texture refs persist, but generated brush meshes still hydrate through a single material batch.
|
- Per-face material and texture refs persist and are editable through Asset Browser-aware controls, but generated brush meshes still hydrate through a single material batch.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user