725 lines
26 KiB
Rust
725 lines
26 KiB
Rust
use super::texture_inputs::{
|
|
ensure_material_texture_binding, material_property_texture_binding,
|
|
material_texture_channel_ui, texture_binding_ui, texture_channel_label,
|
|
upsert_material_texture_binding,
|
|
};
|
|
use super::uv_transform::{material_vec2_value, set_material_vec2_value, uv_transform_section};
|
|
use super::*;
|
|
|
|
use crate::design_system;
|
|
use crate::design_system::color_picker::{color_control, color_control_with_intensity};
|
|
use crate::design_system::controls::{
|
|
scalar_control, section_header, section_header_with_summary, switch,
|
|
};
|
|
use crate::design_system::property_grid::PropertyGridGeometry;
|
|
use crate::design_system::typography::TypeRole;
|
|
use material_schema::MaterialShaderKind;
|
|
|
|
pub fn material_input_schema_editor(
|
|
ui: &mut egui::Ui,
|
|
schema: &MaterialInputSchema,
|
|
inputs: &mut MaterialInputSet,
|
|
texture_assets: &[MaterialTextureCandidate],
|
|
drop_candidate: Option<&MaterialTextureCandidate>,
|
|
) -> MaterialInputEditorResponse {
|
|
material_input_schema_editor_with_inheritance(
|
|
ui,
|
|
schema,
|
|
inputs,
|
|
None,
|
|
texture_assets,
|
|
drop_candidate,
|
|
)
|
|
}
|
|
|
|
pub fn material_input_schema_editor_with_inheritance(
|
|
ui: &mut egui::Ui,
|
|
schema: &MaterialInputSchema,
|
|
inputs: &mut MaterialInputSet,
|
|
inherited: Option<&MaterialInputSet>,
|
|
texture_assets: &[MaterialTextureCandidate],
|
|
drop_candidate: Option<&MaterialTextureCandidate>,
|
|
) -> MaterialInputEditorResponse {
|
|
let mut response = MaterialInputEditorResponse::default();
|
|
let has_standard_packed_inputs = ["occlusion", "roughness", "metallic"]
|
|
.iter()
|
|
.all(|name| schema.inputs.iter().any(|input| input.name == *name));
|
|
let mut groups = schema.groups.clone();
|
|
if groups.is_empty() {
|
|
groups.push(material_schema::MaterialInputGroupDesc {
|
|
id: String::new(),
|
|
display_name: "Properties".into(),
|
|
order: 0,
|
|
advanced: false,
|
|
});
|
|
}
|
|
groups.sort_by_key(|group| group.order);
|
|
for group in groups {
|
|
let mut properties = schema
|
|
.inputs
|
|
.iter()
|
|
.filter(|property| {
|
|
property.group == group.id
|
|
&& matches!(
|
|
property.presentation,
|
|
material_schema::MaterialInputPresentation::Row
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let companions = schema
|
|
.inputs
|
|
.iter()
|
|
.filter(|property| property.group == group.id)
|
|
.filter_map(|property| match &property.presentation {
|
|
material_schema::MaterialInputPresentation::Companion { owner } => {
|
|
Some((owner.as_str(), property))
|
|
}
|
|
material_schema::MaterialInputPresentation::Row => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
properties.sort_by_key(|property| property.order);
|
|
if properties.is_empty() {
|
|
continue;
|
|
}
|
|
if group.id == "uv_transform" {
|
|
material_uv_transform_ui(ui, inputs, inherited);
|
|
// The design-system scope contributes four pixels between vertical items; add the
|
|
// remaining four to reach the authored eight-pixel section gap.
|
|
ui.add_space(4.0);
|
|
continue;
|
|
}
|
|
let open_id = ui.make_persistent_id(("material_input_group_open", &group.id));
|
|
let mut open = ui
|
|
.ctx()
|
|
.data_mut(|data| data.get_persisted::<bool>(open_id))
|
|
.unwrap_or(!group.advanced);
|
|
let context = MaterialInputGroupContext {
|
|
properties: &properties,
|
|
companions: &companions,
|
|
texture_assets,
|
|
drop_candidate,
|
|
};
|
|
let rows = material_input_group_ui(ui, context, &mut open, inputs, |ui, open| {
|
|
if group.id == "surface_inputs" {
|
|
let summary = format!("{} bindings", properties.len());
|
|
section_header_with_summary(ui, "Inputs", Some(&summary), open);
|
|
} else {
|
|
section_header(ui, &group.display_name, open);
|
|
}
|
|
});
|
|
response.accepted_drop |= rows.accepted_drop;
|
|
response.locate = response.locate.take().or(rows.locate);
|
|
response.browse_library |= rows.browse_library;
|
|
ui.ctx()
|
|
.data_mut(|data| data.insert_persisted(open_id, open));
|
|
ui.add_space(4.0);
|
|
}
|
|
if has_standard_packed_inputs {
|
|
super::advanced_preview::unsupported_advanced_preview(ui);
|
|
}
|
|
response
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct MaterialInputGroupContext<'schema, 'view> {
|
|
properties: &'view [&'schema MaterialInputDesc],
|
|
companions: &'view [(&'schema str, &'schema MaterialInputDesc)],
|
|
texture_assets: &'view [MaterialTextureCandidate],
|
|
drop_candidate: Option<&'view MaterialTextureCandidate>,
|
|
}
|
|
|
|
fn material_input_group_ui(
|
|
ui: &mut egui::Ui,
|
|
context: MaterialInputGroupContext<'_, '_>,
|
|
open: &mut bool,
|
|
inputs: &mut MaterialInputSet,
|
|
header_ui: impl FnOnce(&mut egui::Ui, &mut bool),
|
|
) -> MaterialInputEditorResponse {
|
|
let width = ui.available_width().max(1.0);
|
|
let geometry = PropertyGridGeometry::for_width(width);
|
|
let body_open = *open;
|
|
let height = material_input_group_height(width, body_open, context.properties.len());
|
|
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
|
|
let palette = design_system::palette(ui);
|
|
ui.painter().rect(
|
|
rect,
|
|
5.0,
|
|
palette.section,
|
|
egui::Stroke::new(1.0_f32, palette.border),
|
|
egui::StrokeKind::Inside,
|
|
);
|
|
|
|
// The parent owns the complete section rect. Child controls may manage local layout state,
|
|
// but they cannot alter where the next material section begins.
|
|
let mut section = ui.new_child(egui::UiBuilder::new().max_rect(rect));
|
|
section.set_clip_rect(rect.intersect(ui.clip_rect()));
|
|
let header_rect = egui::Rect::from_min_size(
|
|
rect.min,
|
|
egui::vec2(width, design_system::INPUT_SECTION_HEADER_HEIGHT),
|
|
);
|
|
let mut header = section.new_child(egui::UiBuilder::new().max_rect(header_rect));
|
|
header.set_clip_rect(header_rect.intersect(section.clip_rect()));
|
|
header_ui(&mut header, open);
|
|
|
|
if body_open {
|
|
let rows_rect =
|
|
egui::Rect::from_min_max(egui::pos2(rect.left(), header_rect.bottom()), rect.max);
|
|
material_input_rows(&mut section, rows_rect, geometry, context, inputs)
|
|
} else {
|
|
MaterialInputEditorResponse::default()
|
|
}
|
|
}
|
|
|
|
fn material_input_group_height(width: f32, open: bool, rows: usize) -> f32 {
|
|
let body = if open {
|
|
PropertyGridGeometry::for_width(width).row_height * rows as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
design_system::INPUT_SECTION_HEADER_HEIGHT + body
|
|
}
|
|
|
|
fn material_uv_transform_ui(
|
|
ui: &mut egui::Ui,
|
|
inputs: &mut MaterialInputSet,
|
|
inherited: Option<&MaterialInputSet>,
|
|
) {
|
|
let mut offset = material_vec2_value(inputs, "uv_offset", Vec2::ZERO);
|
|
let mut tiling = material_vec2_value(inputs, "uv_tiling", Vec2::ONE);
|
|
let reset_offset = inherited.map_or(Vec2::ZERO, |inputs| {
|
|
material_vec2_value(inputs, "uv_offset", Vec2::ZERO)
|
|
});
|
|
let reset_tiling = inherited.map_or(Vec2::ONE, |inputs| {
|
|
material_vec2_value(inputs, "uv_tiling", Vec2::ONE)
|
|
});
|
|
let original = (offset, tiling);
|
|
uv_transform_section(ui, &mut offset, &mut tiling, reset_offset, reset_tiling);
|
|
if (offset, tiling) != original {
|
|
set_material_vec2_value(inputs, "uv_offset", offset);
|
|
set_material_vec2_value(inputs, "uv_tiling", tiling);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PackedMapMode {
|
|
Arm,
|
|
Separate,
|
|
}
|
|
|
|
pub fn packed_map_mode(inputs: &MaterialInputSet) -> PackedMapMode {
|
|
let channels = [
|
|
("occlusion", TextureChannel::R),
|
|
("roughness", TextureChannel::G),
|
|
("metallic", TextureChannel::B),
|
|
];
|
|
if channels.iter().all(|(name, channel)| {
|
|
inputs
|
|
.texture(name)
|
|
.is_some_and(|binding| binding.channel == *channel)
|
|
}) {
|
|
PackedMapMode::Arm
|
|
} else {
|
|
PackedMapMode::Separate
|
|
}
|
|
}
|
|
|
|
pub fn apply_arm_texture_preset(inputs: &mut MaterialInputSet) {
|
|
let shared_texture = ["occlusion", "roughness", "metallic"]
|
|
.iter()
|
|
.find_map(|name| {
|
|
inputs
|
|
.texture(name)
|
|
.and_then(|binding| binding.texture.clone())
|
|
});
|
|
for (name, channel) in [
|
|
("occlusion", TextureChannel::R),
|
|
("roughness", TextureChannel::G),
|
|
("metallic", TextureChannel::B),
|
|
] {
|
|
let binding = ensure_material_texture_binding(&mut inputs.textures, name);
|
|
binding.channel = channel;
|
|
if let Some(texture) = shared_texture.clone() {
|
|
binding.texture = Some(texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn apply_separate_texture_preset(inputs: &mut MaterialInputSet) {
|
|
for name in ["occlusion", "roughness", "metallic"] {
|
|
ensure_material_texture_binding(&mut inputs.textures, name).channel = TextureChannel::R;
|
|
}
|
|
}
|
|
|
|
fn material_input_rows(
|
|
ui: &mut egui::Ui,
|
|
rows_rect: egui::Rect,
|
|
geometry: PropertyGridGeometry,
|
|
context: MaterialInputGroupContext<'_, '_>,
|
|
inputs: &mut MaterialInputSet,
|
|
) -> MaterialInputEditorResponse {
|
|
let palette = design_system::palette(ui);
|
|
let mut response = MaterialInputEditorResponse::default();
|
|
for (row, property) in context.properties.iter().enumerate() {
|
|
let companion = context
|
|
.companions
|
|
.iter()
|
|
.find_map(|(owner, companion)| (*owner == property.name).then_some(*companion));
|
|
ui.push_id(("material_input_row", &property.name), |ui| {
|
|
let row_rect = egui::Rect::from_min_size(
|
|
rows_rect.min + egui::vec2(0.0, row as f32 * geometry.row_height),
|
|
egui::vec2(rows_rect.width(), geometry.row_height),
|
|
);
|
|
ui.painter().rect_filled(
|
|
row_rect,
|
|
0.0,
|
|
if row.is_multiple_of(2) {
|
|
palette.row_even
|
|
} else {
|
|
palette.row_odd
|
|
},
|
|
);
|
|
let rects = geometry.row_rects(row_rect);
|
|
rect_ui(ui, rects.label, |ui| material_input_label(ui, property));
|
|
let is_color = matches!(property.property_type, ShaderPropertyType::Color);
|
|
rect_ui(
|
|
ui,
|
|
if is_color {
|
|
rects.color_value
|
|
} else {
|
|
rects.value
|
|
},
|
|
|ui| material_input_value_ui(ui, property, companion, inputs),
|
|
);
|
|
if !is_color {
|
|
rect_ui(ui, rects.channel, |ui| {
|
|
material_input_channel_ui(ui, property, inputs)
|
|
});
|
|
}
|
|
if material_input_uses_texture(property) {
|
|
let texture = rect_ui(ui, rects.texture_group, |ui| {
|
|
material_input_texture_ui(
|
|
ui,
|
|
property,
|
|
inputs,
|
|
context.texture_assets,
|
|
context.drop_candidate,
|
|
rects.inline_texture_actions,
|
|
)
|
|
});
|
|
response.accepted_drop |= texture.accepted_drop;
|
|
response.locate = response.locate.take().or(texture.locate);
|
|
response.browse_library |= texture.browse_library;
|
|
}
|
|
});
|
|
}
|
|
response
|
|
}
|
|
|
|
fn rect_ui<R>(
|
|
ui: &mut egui::Ui,
|
|
rect: egui::Rect,
|
|
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
|
) -> R {
|
|
ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
|
|
ui.set_clip_rect(rect.intersect(ui.clip_rect()));
|
|
add_contents(ui)
|
|
})
|
|
.inner
|
|
}
|
|
|
|
pub(super) fn material_input_label(ui: &mut egui::Ui, property: &MaterialInputDesc) {
|
|
let palette = design_system::palette(ui);
|
|
ui.add(
|
|
egui::Label::new(
|
|
TypeRole::Body
|
|
.text(&property.display_name)
|
|
.color(palette.text_secondary),
|
|
)
|
|
.truncate(),
|
|
)
|
|
.on_hover_text(&property.tooltip);
|
|
}
|
|
|
|
pub(super) fn material_input_uses_texture(property: &MaterialInputDesc) -> bool {
|
|
property.texture.is_some() || matches!(property.property_type, ShaderPropertyType::Texture)
|
|
}
|
|
|
|
pub(super) fn material_input_value_ui(
|
|
ui: &mut egui::Ui,
|
|
property: &MaterialInputDesc,
|
|
companion: Option<&MaterialInputDesc>,
|
|
inputs: &mut MaterialInputSet,
|
|
) {
|
|
let MaterialInputSet { values, .. } = inputs;
|
|
ui.horizontal(|ui| {
|
|
if matches!(property.property_type, ShaderPropertyType::Texture) {
|
|
ui.label(egui::RichText::new("Texture").small().color(TEXT_DIM));
|
|
} else {
|
|
let default = property
|
|
.default_value
|
|
.clone()
|
|
.unwrap_or_else(|| default_value_for_shader_property(&property.property_type));
|
|
let mut value = values
|
|
.iter()
|
|
.find(|value| value.name == property.name)
|
|
.map(|parameter| parameter.value.clone())
|
|
.unwrap_or(default);
|
|
if !parameter_value_matches_property(&value, &property.property_type) {
|
|
value = default_value_for_shader_property(&property.property_type);
|
|
}
|
|
let original = value.clone();
|
|
if matches!(property.property_type, ShaderPropertyType::Color)
|
|
&& companion.is_some_and(|input| input.name == "emissive_intensity")
|
|
{
|
|
let companion = companion.expect("checked companion");
|
|
let companion_default = companion
|
|
.default_value
|
|
.clone()
|
|
.unwrap_or(MaterialParameterValue::Float(0.0));
|
|
let mut companion_value = values
|
|
.iter()
|
|
.find(|value| value.name == companion.name)
|
|
.map(|parameter| parameter.value.clone())
|
|
.unwrap_or(companion_default);
|
|
if !parameter_value_matches_property(&companion_value, &companion.property_type) {
|
|
companion_value = default_value_for_shader_property(&companion.property_type);
|
|
}
|
|
let original_companion = companion_value.clone();
|
|
if let (
|
|
MaterialParameterValue::Color(color),
|
|
MaterialParameterValue::Float(intensity),
|
|
) = (&mut value, &mut companion_value)
|
|
{
|
|
let mut rgba = [color.r, color.g, color.b, color.a];
|
|
let picker = color_control_with_intensity(
|
|
ui,
|
|
"material_emissive_color",
|
|
&property.display_name,
|
|
&mut rgba,
|
|
intensity,
|
|
);
|
|
if picker.changed {
|
|
*color = ColorDesc {
|
|
r: rgba[0],
|
|
g: rgba[1],
|
|
b: rgba[2],
|
|
a: rgba[3],
|
|
};
|
|
}
|
|
}
|
|
if value != original {
|
|
upsert_material_parameter(values, &property.name, value);
|
|
}
|
|
if companion_value != original_companion {
|
|
upsert_material_parameter(values, &companion.name, companion_value);
|
|
}
|
|
} else {
|
|
material_parameter_control_ui(
|
|
ui,
|
|
&property.display_name,
|
|
&property.property_type,
|
|
&mut value,
|
|
);
|
|
if value != original {
|
|
upsert_material_parameter(values, &property.name, value);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn upsert_material_parameter(
|
|
values: &mut Vec<MaterialParameter>,
|
|
name: &str,
|
|
value: MaterialParameterValue,
|
|
) {
|
|
if let Some(parameter) = values.iter_mut().find(|parameter| parameter.name == name) {
|
|
parameter.value = value;
|
|
} else {
|
|
values.push(MaterialParameter {
|
|
name: name.to_string(),
|
|
value,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn material_input_channel_ui(
|
|
ui: &mut egui::Ui,
|
|
property: &MaterialInputDesc,
|
|
inputs: &mut MaterialInputSet,
|
|
) {
|
|
let palette = design_system::palette(ui);
|
|
if property
|
|
.texture
|
|
.as_ref()
|
|
.is_some_and(|texture| texture.allow_channel_override)
|
|
{
|
|
let original = material_property_texture_binding(&inputs.textures, property);
|
|
let mut binding = original.clone();
|
|
material_texture_channel_ui(ui, property, &mut binding);
|
|
if binding != original {
|
|
upsert_material_texture_binding(&mut inputs.textures, binding);
|
|
}
|
|
} else {
|
|
let channel = property.texture.as_ref().map(|texture| {
|
|
if matches!(texture.semantic, material_schema::TextureSemantic::Color) {
|
|
"RGB"
|
|
} else {
|
|
texture_channel_label(texture.default_channel)
|
|
}
|
|
});
|
|
if let Some(channel) = channel {
|
|
readonly_channel(ui, channel);
|
|
} else {
|
|
ui.label(TypeRole::Body.text("—").color(palette.text_muted));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn material_input_texture_ui(
|
|
ui: &mut egui::Ui,
|
|
property: &MaterialInputDesc,
|
|
inputs: &mut MaterialInputSet,
|
|
texture_assets: &[MaterialTextureCandidate],
|
|
drop_candidate: Option<&MaterialTextureCandidate>,
|
|
inline_actions: bool,
|
|
) -> TextureSlotResponse {
|
|
if !material_input_uses_texture(property) {
|
|
return TextureSlotResponse::default();
|
|
}
|
|
let original = material_property_texture_binding(&inputs.textures, property);
|
|
let mut binding = original.clone();
|
|
let response = texture_binding_ui(
|
|
ui,
|
|
&mut binding,
|
|
texture_assets,
|
|
drop_candidate,
|
|
inline_actions,
|
|
);
|
|
if binding != original {
|
|
upsert_material_texture_binding(&mut inputs.textures, binding);
|
|
}
|
|
response
|
|
}
|
|
|
|
pub(super) fn material_parameter_control_ui(
|
|
ui: &mut egui::Ui,
|
|
label: &str,
|
|
property_type: &ShaderPropertyType,
|
|
value: &mut MaterialParameterValue,
|
|
) {
|
|
if !parameter_value_matches_property(value, property_type) {
|
|
*value = default_value_for_shader_property(property_type);
|
|
}
|
|
match value {
|
|
MaterialParameterValue::Bool(value) => {
|
|
switch(ui, value);
|
|
}
|
|
MaterialParameterValue::Float(value) => {
|
|
let (min, max) = match property_type {
|
|
ShaderPropertyType::Float { min, max } => (*min, *max),
|
|
_ => (None, None),
|
|
};
|
|
match (min, max) {
|
|
(Some(min), Some(max)) => {
|
|
scalar_control(ui, label, value, min..=max, usize::from(max <= 1_000.0) * 2);
|
|
}
|
|
_ => {
|
|
ui.add(egui::DragValue::new(value).speed(0.01));
|
|
}
|
|
}
|
|
}
|
|
MaterialParameterValue::Vec2(value) => {
|
|
ui.add(egui::DragValue::new(&mut value.x).speed(0.01).prefix("X "));
|
|
ui.add(egui::DragValue::new(&mut value.y).speed(0.01).prefix("Y "));
|
|
}
|
|
MaterialParameterValue::Vec3(value) => {
|
|
ui.add(egui::DragValue::new(&mut value.x).speed(0.01).prefix("X "));
|
|
ui.add(egui::DragValue::new(&mut value.y).speed(0.01).prefix("Y "));
|
|
ui.add(egui::DragValue::new(&mut value.z).speed(0.01).prefix("Z "));
|
|
}
|
|
MaterialParameterValue::Color(value) => {
|
|
let mut rgba = [value.r, value.g, value.b, value.a];
|
|
let response = color_control(ui, "material_color", label, &mut rgba);
|
|
if response.changed {
|
|
*value = ColorDesc {
|
|
r: rgba[0],
|
|
g: rgba[1],
|
|
b: rgba[2],
|
|
a: rgba[3],
|
|
};
|
|
}
|
|
}
|
|
MaterialParameterValue::Enum(value) => {
|
|
let options = match property_type {
|
|
ShaderPropertyType::Enum { options } => options.as_slice(),
|
|
_ => &[],
|
|
};
|
|
egui::ComboBox::from_id_salt(("material_enum", value.as_str()))
|
|
.selected_text(value.as_str())
|
|
.show_ui(ui, |ui| {
|
|
for option in options {
|
|
ui.selectable_value(value, option.clone(), option);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn readonly_channel(ui: &mut egui::Ui, channel: &str) {
|
|
let palette = design_system::palette(ui);
|
|
let (rect, response) = ui.allocate_exact_size(egui::vec2(48.0, 22.0), egui::Sense::hover());
|
|
ui.painter().rect(
|
|
rect,
|
|
4.0,
|
|
palette.control,
|
|
egui::Stroke::new(1.0_f32, palette.border),
|
|
egui::StrokeKind::Inside,
|
|
);
|
|
ui.painter().text(
|
|
rect.left_center() + egui::vec2(9.0, 0.0),
|
|
egui::Align2::LEFT_CENTER,
|
|
channel,
|
|
TypeRole::Body.font(),
|
|
palette.text_secondary,
|
|
);
|
|
ui.painter().text(
|
|
rect.right_center() - egui::vec2(8.0, 0.0),
|
|
egui::Align2::CENTER_CENTER,
|
|
egui_phosphor_icons::icons::CARET_DOWN.as_str(),
|
|
egui::FontId::new(9.0, egui::FontFamily::Name("phosphor-bold".into())),
|
|
palette.text_muted,
|
|
);
|
|
response.on_hover_text("Channel is fixed by this material input");
|
|
}
|
|
|
|
pub fn default_value_for_shader_property(
|
|
property_type: &ShaderPropertyType,
|
|
) -> MaterialParameterValue {
|
|
match property_type {
|
|
ShaderPropertyType::Bool => MaterialParameterValue::Bool(false),
|
|
ShaderPropertyType::Float { .. } => MaterialParameterValue::Float(0.0),
|
|
ShaderPropertyType::Vec2 => MaterialParameterValue::Vec2(Vec2::ZERO),
|
|
ShaderPropertyType::Vec3 => MaterialParameterValue::Vec3(Vec3::ZERO),
|
|
ShaderPropertyType::Color => MaterialParameterValue::Color(ColorDesc::default()),
|
|
ShaderPropertyType::Enum { options } => {
|
|
MaterialParameterValue::Enum(options.first().cloned().unwrap_or_default())
|
|
}
|
|
ShaderPropertyType::Texture => MaterialParameterValue::Float(0.0),
|
|
}
|
|
}
|
|
|
|
pub fn parameter_value_matches_property(
|
|
value: &MaterialParameterValue,
|
|
property_type: &ShaderPropertyType,
|
|
) -> bool {
|
|
matches!(
|
|
(value, property_type),
|
|
(MaterialParameterValue::Bool(_), ShaderPropertyType::Bool)
|
|
| (
|
|
MaterialParameterValue::Float(_),
|
|
ShaderPropertyType::Float { .. }
|
|
)
|
|
| (MaterialParameterValue::Vec2(_), ShaderPropertyType::Vec2)
|
|
| (MaterialParameterValue::Vec3(_), ShaderPropertyType::Vec3)
|
|
| (MaterialParameterValue::Color(_), ShaderPropertyType::Color)
|
|
| (
|
|
MaterialParameterValue::Enum(_),
|
|
ShaderPropertyType::Enum { .. }
|
|
)
|
|
)
|
|
}
|
|
|
|
pub fn compact_text_edit(ui: &mut egui::Ui, label: &str, value: &mut String) {
|
|
ui.horizontal(|ui| {
|
|
ui.add_sized([92.0, 20.0], egui::Label::new(label));
|
|
ui.add_sized(
|
|
[fit_width(ui, 100.0, f32::INFINITY), 22.0],
|
|
egui::TextEdit::singleline(value),
|
|
);
|
|
});
|
|
}
|
|
|
|
pub fn shader_kind_picker(ui: &mut egui::Ui, kind: &mut MaterialShaderKind) {
|
|
ui.horizontal(|ui| {
|
|
ui.add_sized([92.0, 20.0], egui::Label::new("Shader"));
|
|
shader_kind_combo(ui, kind, "asset_material_shader_kind");
|
|
});
|
|
}
|
|
|
|
pub fn shader_kind_combo(
|
|
ui: &mut egui::Ui,
|
|
kind: &mut MaterialShaderKind,
|
|
salt: impl std::hash::Hash,
|
|
) {
|
|
egui::ComboBox::from_id_salt(salt)
|
|
.selected_text(match kind {
|
|
MaterialShaderKind::StandardLit => "Standard Lit",
|
|
MaterialShaderKind::Unlit => "Unlit",
|
|
MaterialShaderKind::Custom => "Custom Surface",
|
|
})
|
|
.width(150.0_f32.min(ui.available_width().max(1.0)))
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(kind, MaterialShaderKind::StandardLit, "Standard Lit");
|
|
ui.selectable_value(kind, MaterialShaderKind::Unlit, "Unlit");
|
|
ui.selectable_value(kind, MaterialShaderKind::Custom, "Custom Surface");
|
|
});
|
|
}
|
|
|
|
pub fn optional_path_edit(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) {
|
|
ui.horizontal(|ui| {
|
|
ui.add_sized([92.0, 20.0], egui::Label::new(label));
|
|
let mut text = value.clone().unwrap_or_default();
|
|
if ui
|
|
.add_sized(
|
|
[fit_width(ui, 80.0, f32::INFINITY), 22.0],
|
|
egui::TextEdit::singleline(&mut text),
|
|
)
|
|
.changed()
|
|
{
|
|
*value = if text.trim().is_empty() {
|
|
None
|
|
} else {
|
|
Some(text)
|
|
};
|
|
}
|
|
if ui.button("Clear").clicked() {
|
|
*value = None;
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn input_group_parent_owns_exact_reference_and_compact_heights() {
|
|
assert_eq!(material_input_group_height(569.0, true, 6), 216.0);
|
|
assert_eq!(material_input_group_height(369.0, true, 6), 372.0);
|
|
assert_eq!(material_input_group_height(569.0, false, 6), 24.0);
|
|
}
|
|
|
|
#[test]
|
|
fn rendering_missing_standard_defaults_does_not_author_or_dirty_inputs() {
|
|
let context = egui::Context::default();
|
|
context.set_fonts(crate::fonts::font_definitions());
|
|
let mut inputs = MaterialInputSet::default();
|
|
let original = inputs.clone();
|
|
let schema = material_schema::standard_lit_input_schema();
|
|
let _ = context.run_ui(
|
|
egui::RawInput {
|
|
screen_rect: Some(egui::Rect::from_min_size(
|
|
egui::Pos2::ZERO,
|
|
egui::vec2(700.0, 900.0),
|
|
)),
|
|
..Default::default()
|
|
},
|
|
|ui| {
|
|
material_input_schema_editor(ui, &schema, &mut inputs, &[], None);
|
|
},
|
|
);
|
|
assert_eq!(inputs, original);
|
|
}
|
|
}
|