use crate::ecs::EditorWorld;
use crate::systems::retained_ui::{Action, UiHandles};
use nightshade::prelude::*;
use std::collections::HashMap;
#[derive(Default, Clone)]
pub struct InspectorHandles {
pub panel: Entity,
pub header_label: Entity,
pub clear_button: Entity,
pub body_root: Entity,
pub body_entities: Vec<Entity>,
pub bindings: HashMap<Entity, InspectorBinding>,
pub editing_state: HashMap<Entity, bool>,
pub pre_drag_transforms: HashMap<Entity, LocalTransform>,
pub pre_drag_components:
HashMap<(Entity, crate::undo::SnapshotKind), crate::undo::ComponentSnapshot>,
pub pending_picker_edits: HashMap<Entity, PickerEditSession>,
pub tag_remove_buttons: HashMap<Entity, String>,
pub tag_input: Option<Entity>,
pub dirty: bool,
}
#[derive(Clone)]
pub struct PickerEditSession {
pub target: Entity,
pub field: InspectorField,
pub before: crate::undo::ComponentSnapshot,
}
#[derive(Clone, Copy)]
pub struct InspectorBinding {
pub entity: Entity,
pub field: InspectorField,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum InspectorField {
Name,
TranslationX,
TranslationY,
TranslationZ,
EulerX,
EulerY,
EulerZ,
ScaleX,
ScaleY,
ScaleZ,
Visibility,
CastsShadow,
LightIntensity,
LightCastsShadow,
LightColor,
LightRange,
LightType,
LightInnerCone,
LightOuterCone,
LightShadowBias,
LightShadowResolution,
LightShadowDistance,
LightCookieTexture,
AmbientLight,
BloomEnabled,
BloomIntensity,
BloomThreshold,
BloomKnee,
BloomFilterRadius,
SsaoEnabled,
SsaoRadius,
SsaoIntensity,
SsaoBias,
SsaoSampleCount,
SsaoVisualization,
SsgiEnabled,
SsgiRadius,
SsgiIntensity,
SsgiMaxSteps,
SsrEnabled,
SsrMaxSteps,
SsrThickness,
SsrMaxDistance,
SsrStride,
SsrFadeStart,
SsrFadeEnd,
SsrIntensity,
FogEnabled,
FogColor,
FogStart,
FogEnd,
DofEnabled,
DofFocusDistance,
DofFocusRange,
DofMaxBlurRadius,
DofBokehThreshold,
DofBokehIntensity,
DofQuality,
FxaaEnabled,
RenderScale,
IblBlendFactor,
PbrDebug,
UnlitMode,
SelectionOutlineEnabled,
SelectionOutlineColor,
Exposure,
Saturation,
Contrast,
Brightness,
Gamma,
Particle(ParticleField),
Decal(DecalField),
Audio(AudioField),
RigidBody(RigidBodyField),
Collider(ColliderField),
Navmesh(NavmeshField),
Camera(CameraField),
Animation(AnimationField),
Material(MaterialField),
Text(TextField),
Grass(GrassField),
GrassInteractor(GrassInteractorField),
Misc(MiscField),
AddComponent(ComponentKind),
RemoveComponent(ComponentKind),
CharacterController(CharacterControllerField),
Instance(InstanceField),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum InstanceField {
TranslationX(usize),
TranslationY(usize),
TranslationZ(usize),
EulerX(usize),
EulerY(usize),
EulerZ(usize),
ScaleX(usize),
ScaleY(usize),
ScaleZ(usize),
Tint(usize),
Add,
Remove(usize),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CharacterControllerField {
Shape,
CapsuleHalfHeight,
CapsuleRadius,
BallRadius,
CuboidX,
CuboidY,
CuboidZ,
MaxSpeed,
Acceleration,
JumpImpulse,
Scale,
CrouchEnabled,
CrouchSpeedMultiplier,
SprintSpeedMultiplier,
StandingHalfHeight,
CrouchingHalfHeight,
ConfigOffset,
MaxSlopeClimb,
MinSlopeSlide,
EngineInputEnabled,
FrictionRate,
AboveMaxFrictionRate,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ParticleField {
Enabled,
EmitterType,
Shape,
ShapeRadius,
ShapeAngle,
ShapeHeight,
ShapeBoxX,
ShapeBoxY,
ShapeBoxZ,
PositionX,
PositionY,
PositionZ,
DirectionX,
DirectionY,
DirectionZ,
SpawnRate,
BurstCount,
LifetimeMin,
LifetimeMax,
VelocityMin,
VelocityMax,
VelocitySpread,
GravityX,
GravityY,
GravityZ,
Drag,
SizeStart,
SizeEnd,
EmissiveStrength,
OneShot,
TurbulenceStrength,
TurbulenceFrequency,
TextureIndex,
SizeCurveTime(usize),
SizeCurveValue(usize),
AddSizeCurveKey,
RemoveSizeCurveKey(usize),
OpacityCurveTime(usize),
OpacityCurveValue(usize),
AddOpacityCurveKey,
RemoveOpacityCurveKey(usize),
DeathSubEmitter,
TrailSubEmitter,
TrailSpawnInterval,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DecalField {
EmissiveStrength,
Color,
SizeX,
SizeY,
Depth,
NormalThreshold,
FadeStart,
FadeEnd,
Texture,
EmissiveTexture,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AudioField {
AudioRef,
Volume,
Looping,
Playing,
Spatial,
Bus,
MinDistance,
MaxDistance,
ReverbZoneName(usize),
ReverbZoneSend(usize),
AddReverbZone,
RemoveReverbZone(usize),
RandomPick,
RandomClip(usize),
AddRandomClip,
RemoveRandomClip(usize),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RigidBodyField {
BodyType,
Mass,
LinvelX,
LinvelY,
LinvelZ,
AngvelX,
AngvelY,
AngvelZ,
Ccd,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ColliderField {
Shape,
BallRadius,
CuboidX,
CuboidY,
CuboidZ,
CapsuleHalfHeight,
CapsuleRadius,
CylinderHalfHeight,
CylinderRadius,
ConeHalfHeight,
ConeRadius,
Friction,
Restitution,
Density,
IsSensor,
CollisionMemberships,
CollisionFilter,
SolverMemberships,
SolverFilter,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum NavmeshField {
MovementSpeed,
ArrivalThreshold,
PathRecalculationThreshold,
AgentRadius,
AgentHeight,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CameraField {
ProjectionKind,
FovDegrees,
AspectOverride,
AspectValue,
NearPlane,
FarOverride,
FarValue,
OrthoX,
OrthoY,
OrthoNear,
OrthoFar,
SetActive,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnimationField {
Playing,
Looping,
Speed,
PlayAll,
ClipSelect,
Time,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MaterialField {
BaseColor,
Roughness,
Metallic,
EmissiveFactor,
EmissiveStrength,
AlphaMode,
AlphaCutoff,
Unlit,
DoubleSided,
NormalScale,
OcclusionStrength,
Ior,
Transmission,
Thickness,
ClearcoatFactor,
ClearcoatRoughness,
SheenColor,
SheenRoughness,
IridescenceFactor,
AnisotropyStrength,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TextField {
Content,
FontSize,
Color,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GrassField {
Enabled,
WindStrength,
WindFrequency,
WindDirX,
WindDirZ,
InteractionRadius,
InteractionStrength,
CastShadows,
ReceiveShadows,
BladesPerPatch,
PatchSize,
StreamRadius,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GrassInteractorField {
Radius,
Strength,
}
pub struct SceneStructureSnapshot {
pub layers: Vec<nightshade::ecs::scene::SceneLayerConfig>,
pub chunks: Vec<nightshade::ecs::scene::SceneChunkConfig>,
pub entity_layer: Option<nightshade::ecs::scene::LayerId>,
pub entity_chunk: Option<nightshade::ecs::scene::ChunkId>,
pub joints: Vec<nightshade::ecs::scene::SceneJointConnection>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MiscField {
RenderLayer,
CullingMaskBit(u8),
CameraCullingMaskBit(u8),
IgnoreParentScale,
LinesAlwaysOnTop,
LinesClear,
SceneLayerAssignment,
SceneChunkAssignment,
SceneAddLayer,
SceneAddChunk,
SceneRemoveLayer(u32),
SceneRemoveChunk(u32),
SceneLayerName(u32),
SceneLayerOrder(u32),
SceneLayerEnabled(u32),
SceneChunkName(u32),
SceneChunkLoadDistance(u32),
SceneChunkUnloadDistance(u32),
SceneChunkBoundsMinX(u32),
SceneChunkBoundsMinY(u32),
SceneChunkBoundsMinZ(u32),
SceneChunkBoundsMaxX(u32),
SceneChunkBoundsMaxY(u32),
SceneChunkBoundsMaxZ(u32),
AddFixedJoint,
AddRevoluteJoint,
AddPrismaticJoint,
AddSphericalJoint,
AddRopeJoint,
AddSpringJoint,
RemoveJoint(usize),
ToggleJointCollisions(usize),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ComponentKind {
ParticleEmitter,
Decal,
AudioSource,
RigidBody,
Collider,
CharacterController,
NavmeshAgent,
Camera,
GrassRegion,
GrassInteractor,
Text,
AnimationPlayer,
Visibility,
CastsShadow,
Light,
RenderLayer,
CullingMask,
CameraCullingMask,
IgnoreParentScale,
AudioListener,
CollisionListener,
PhysicsInterpolation,
MorphWeights,
MaterialVariants,
PanOrbitCamera,
ThirdPersonCamera,
CameraEnvironment,
CameraPostProcess,
ConstrainedAspect,
ViewportUpdateMode,
}
impl InspectorField {
fn is_transform(self) -> bool {
matches!(
self,
Self::TranslationX
| Self::TranslationY
| Self::TranslationZ
| Self::EulerX
| Self::EulerY
| Self::EulerZ
| Self::ScaleX
| Self::ScaleY
| Self::ScaleZ
)
}
fn snapshot_kind(self) -> Option<crate::undo::SnapshotKind> {
use crate::undo::SnapshotKind;
match self {
Self::Name => Some(SnapshotKind::Name),
Self::Visibility => Some(SnapshotKind::Visibility),
Self::CastsShadow => Some(SnapshotKind::CastsShadow),
Self::LightIntensity
| Self::LightRange
| Self::LightColor
| Self::LightCastsShadow
| Self::LightType
| Self::LightInnerCone
| Self::LightOuterCone
| Self::LightShadowBias
| Self::LightShadowResolution
| Self::LightShadowDistance
| Self::LightCookieTexture => Some(SnapshotKind::Light),
Self::Particle(_) => Some(SnapshotKind::ParticleEmitter),
Self::Decal(_) => Some(SnapshotKind::Decal),
Self::Audio(_) => Some(SnapshotKind::AudioSource),
Self::RigidBody(_) => Some(SnapshotKind::RigidBody),
Self::Collider(_) => Some(SnapshotKind::Collider),
Self::Navmesh(_) => Some(SnapshotKind::NavmeshAgent),
Self::Camera(_) => Some(SnapshotKind::Camera),
Self::Animation(_) => Some(SnapshotKind::AnimationPlayer),
Self::Material(_) => Some(SnapshotKind::Material),
Self::Text(_) => Some(SnapshotKind::Text),
Self::Grass(_) => Some(SnapshotKind::GrassRegion),
Self::GrassInteractor(_) => Some(SnapshotKind::GrassInteractor),
Self::Misc(MiscField::RenderLayer) => Some(SnapshotKind::RenderLayer),
Self::Misc(MiscField::CullingMaskBit(_)) => Some(SnapshotKind::CullingMask),
Self::Misc(MiscField::CameraCullingMaskBit(_)) => Some(SnapshotKind::CameraCullingMask),
Self::Misc(MiscField::IgnoreParentScale) => Some(SnapshotKind::IgnoreParentScale),
Self::CharacterController(_) => Some(SnapshotKind::CharacterController),
Self::AddComponent(kind) | Self::RemoveComponent(kind) => {
component_kind_snapshot_kind(kind)
}
_ => None,
}
}
}
fn component_kind_snapshot_kind(kind: ComponentKind) -> Option<crate::undo::SnapshotKind> {
use crate::undo::SnapshotKind;
Some(match kind {
ComponentKind::ParticleEmitter => SnapshotKind::ParticleEmitter,
ComponentKind::Decal => SnapshotKind::Decal,
ComponentKind::AudioSource => SnapshotKind::AudioSource,
ComponentKind::RigidBody => SnapshotKind::RigidBody,
ComponentKind::Collider => SnapshotKind::Collider,
ComponentKind::CharacterController => SnapshotKind::CharacterController,
ComponentKind::NavmeshAgent => SnapshotKind::NavmeshAgent,
ComponentKind::Camera => SnapshotKind::Camera,
ComponentKind::GrassRegion => SnapshotKind::GrassRegion,
ComponentKind::GrassInteractor => SnapshotKind::GrassInteractor,
ComponentKind::Text => SnapshotKind::Text,
ComponentKind::AnimationPlayer => SnapshotKind::AnimationPlayer,
ComponentKind::Visibility => SnapshotKind::Visibility,
ComponentKind::CastsShadow => SnapshotKind::CastsShadow,
ComponentKind::Light => SnapshotKind::Light,
ComponentKind::RenderLayer => SnapshotKind::RenderLayer,
ComponentKind::CullingMask => SnapshotKind::CullingMask,
ComponentKind::CameraCullingMask => SnapshotKind::CameraCullingMask,
ComponentKind::IgnoreParentScale => SnapshotKind::IgnoreParentScale,
ComponentKind::AudioListener
| ComponentKind::CollisionListener
| ComponentKind::PhysicsInterpolation
| ComponentKind::MorphWeights
| ComponentKind::MaterialVariants
| ComponentKind::PanOrbitCamera
| ComponentKind::ThirdPersonCamera
| ComponentKind::CameraEnvironment
| ComponentKind::CameraPostProcess
| ComponentKind::ConstrainedAspect
| ComponentKind::ViewportUpdateMode => return None,
})
}
pub fn build(tree: &mut UiTreeBuilder) -> InspectorHandles {
let panel = tree.add_docked_panel_right("inspector", "Inspector", 320.0);
let content = super::panel_content(tree, panel);
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size;
let text_color = theme.text_color;
let mut header_label = Entity::default();
let mut clear_button = Entity::default();
let mut body_root = Entity::default();
tree.in_parent(content, |tree| {
let header_row = tree.add_node().size(100.pct(), (28.0).px()).entity();
if let Some(node) = tree.world_mut().ui.get_ui_layout_node_mut(header_row) {
node.flow_layout = Some(FlowLayout {
direction: FlowDirection::Horizontal,
padding: 4.0,
spacing: 4.0,
alignment: FlowAlignment::Start,
cross_alignment: FlowAlignment::Center,
wrap: false,
});
}
tree.in_parent(header_row, |tree| {
header_label = tree
.add_node()
.flow_child(Rl(vec2(70.0, 0.0)) + Ab(vec2(0.0, 22.0)))
.with_text("World", font)
.text_left()
.color_raw::<UiBase>(text_color)
.entity();
clear_button = tree
.add_node()
.size((60.0).px(), (22.0).px())
.with_text("Clear", font * 0.85)
.text_center()
.color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
.color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.entity();
});
let scroll = tree.add_scroll_area_fill(0.0, 0.0);
body_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
.map(|d| d.content_entity)
.unwrap_or(scroll);
});
InspectorHandles {
panel,
header_label,
clear_button,
body_root,
body_entities: Vec::new(),
bindings: HashMap::new(),
editing_state: HashMap::new(),
pre_drag_transforms: HashMap::new(),
pre_drag_components: HashMap::new(),
pending_picker_edits: HashMap::new(),
tag_remove_buttons: HashMap::new(),
tag_input: None,
dirty: true,
}
}
pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
let mut clear_selection = false;
let mut field_changes: Vec<(Entity, InspectorField, FieldValue)> = Vec::new();
let mut picker_changes: Vec<(Entity, Entity, InspectorField, FieldValue)> = Vec::new();
let mut drag_ends: Vec<(Entity, InspectorField)> = Vec::new();
let mut tag_to_add: Option<String> = None;
let mut tag_to_remove: Option<String> = None;
for event in ui_events(world) {
match event {
UiEvent::ButtonClicked(entity) if *entity == handles.inspector.clear_button => {
clear_selection = true;
}
UiEvent::ButtonClicked(entity) => {
if let Some(tag) = handles.inspector.tag_remove_buttons.get(entity) {
tag_to_remove = Some(tag.clone());
} else if let Some(binding) = handles.inspector.bindings.get(entity) {
field_changes.push((binding.entity, binding.field, FieldValue::Bool(true)));
}
}
UiEvent::DragValueChanged { entity, value } => {
if let Some(binding) = handles.inspector.bindings.get(entity) {
field_changes.push((binding.entity, binding.field, FieldValue::Float(*value)));
}
}
UiEvent::CheckboxChanged { entity, value } => {
if let Some(binding) = handles.inspector.bindings.get(entity) {
field_changes.push((binding.entity, binding.field, FieldValue::Bool(*value)));
}
}
UiEvent::TextInputSubmitted { entity, text } => {
if Some(*entity) == handles.inspector.tag_input {
let trimmed = text.trim();
if !trimmed.is_empty() {
tag_to_add = Some(trimmed.to_string());
}
} else if let Some(binding) = handles.inspector.bindings.get(entity) {
field_changes.push((
binding.entity,
binding.field,
FieldValue::Text(text.clone()),
));
}
}
UiEvent::ColorPickerChanged { entity, color } => {
if let Some(binding) = handles.inspector.bindings.get(entity) {
picker_changes.push((
*entity,
binding.entity,
binding.field,
FieldValue::Color(*color),
));
}
}
UiEvent::DropdownChanged {
entity,
selected_index,
} => {
if let Some(binding) = handles.inspector.bindings.get(entity) {
field_changes.push((
binding.entity,
binding.field,
FieldValue::Index(*selected_index),
));
}
}
_ => {}
}
}
for (widget_entity, binding) in &handles.inspector.bindings {
if let Some(data) = world.ui.get_ui_drag_value(*widget_entity) {
let was_editing = editor_world
.resources
.ui_handles
.inspector
.editing_state
.get(widget_entity)
.copied()
.unwrap_or(false);
if was_editing && !data.editing {
drag_ends.push((binding.entity, binding.field));
}
editor_world
.resources
.ui_handles
.inspector
.editing_state
.insert(*widget_entity, data.editing);
}
}
if clear_selection {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ClearSelection);
}
if let Some(tag) = tag_to_add {
editor_world
.resources
.ui_interaction
.actions
.push(Action::AddTagToSelected(tag));
if let Some(input) = editor_world.resources.ui_handles.inspector.tag_input {
ui_text_input_set_value(world, input, "");
}
}
if let Some(tag) = tag_to_remove {
editor_world
.resources
.ui_interaction
.actions
.push(Action::RemoveTagFromSelected(tag));
}
let mut pre_drag_transforms = std::mem::take(
&mut editor_world
.resources
.ui_handles
.inspector
.pre_drag_transforms,
);
let mut pre_drag_components = std::mem::take(
&mut editor_world
.resources
.ui_handles
.inspector
.pre_drag_components,
);
let mut pending_picker_edits = std::mem::take(
&mut editor_world
.resources
.ui_handles
.inspector
.pending_picker_edits,
);
for (entity, field, value) in field_changes {
if field.is_transform()
&& !pre_drag_transforms.contains_key(&entity)
&& let Some(transform) = world.core.get_local_transform(entity).copied()
{
pre_drag_transforms.insert(entity, transform);
}
let snapshot_kind = field.snapshot_kind();
let is_drag_value = matches!(value, FieldValue::Float(_));
if is_drag_value
&& let Some(kind) = snapshot_kind
&& !pre_drag_components.contains_key(&(entity, kind))
&& let Some(snap) = crate::undo::ComponentSnapshot::capture(world, entity, kind)
{
pre_drag_components.insert((entity, kind), snap);
}
let before = if is_drag_value {
None
} else {
snapshot_kind
.and_then(|kind| crate::undo::ComponentSnapshot::capture(world, entity, kind))
};
let needs_header_refresh = matches!(field, InspectorField::Name);
let needs_rebuild = matches!(value, FieldValue::Index(_));
apply_field_change(editor_world, world, entity, field, value);
if needs_header_refresh || needs_rebuild {
editor_world.resources.ui_handles.inspector.dirty = true;
}
if let (Some(kind), Some(before)) = (snapshot_kind, before)
&& let Some(after) = crate::undo::ComponentSnapshot::capture(world, entity, kind)
&& before != after
&& let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
{
editor_world.resources.undo.push(
crate::undo::UndoableOperation::ComponentChanged {
uuid,
old: Box::new(before),
new: Box::new(after),
},
"Inspector edit",
);
}
}
let picker_widgets_active: std::collections::HashSet<Entity> =
picker_changes.iter().map(|(widget, ..)| *widget).collect();
for (widget_entity, target, field, value) in picker_changes {
if let Some(kind) = field.snapshot_kind()
&& !pending_picker_edits.contains_key(&widget_entity)
&& let Some(before) = crate::undo::ComponentSnapshot::capture(world, target, kind)
{
pending_picker_edits.insert(
widget_entity,
PickerEditSession {
target,
field,
before,
},
);
}
apply_field_change(editor_world, world, target, field, value);
}
let stale_pickers: Vec<Entity> = pending_picker_edits
.keys()
.copied()
.filter(|widget| !picker_widgets_active.contains(widget))
.collect();
for widget_entity in stale_pickers {
let Some(session) = pending_picker_edits.remove(&widget_entity) else {
continue;
};
let Some(kind) = session.field.snapshot_kind() else {
continue;
};
let Some(after) = crate::undo::ComponentSnapshot::capture(world, session.target, kind)
else {
continue;
};
if session.before == after {
continue;
}
let Some(uuid) = editor_world.resources.editor_scene.uuid_for(session.target) else {
continue;
};
editor_world.resources.undo.push(
crate::undo::UndoableOperation::ComponentChanged {
uuid,
old: Box::new(session.before),
new: Box::new(after),
},
"Inspector edit",
);
}
for (entity, field) in drag_ends {
if field.is_transform() {
if let (Some(start), Some(end)) = (
pre_drag_transforms.remove(&entity),
world.core.get_local_transform(entity).copied(),
) && start != end
&& let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
{
editor_world.resources.undo.push(
crate::undo::UndoableOperation::TransformChanged {
uuid,
old: start,
new: end,
},
"Inspector transform",
);
editor_world.resources.ui_handles.inspector.dirty = true;
}
} else if let Some(kind) = field.snapshot_kind() {
let key = (entity, kind);
if let (Some(before), Some(after)) = (
pre_drag_components.remove(&key),
crate::undo::ComponentSnapshot::capture(world, entity, kind),
) && before != after
&& let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
{
editor_world.resources.undo.push(
crate::undo::UndoableOperation::ComponentChanged {
uuid,
old: Box::new(before),
new: Box::new(after),
},
"Inspector edit",
);
}
}
}
editor_world
.resources
.ui_handles
.inspector
.pre_drag_transforms = pre_drag_transforms;
editor_world
.resources
.ui_handles
.inspector
.pre_drag_components = pre_drag_components;
editor_world
.resources
.ui_handles
.inspector
.pending_picker_edits = pending_picker_edits;
}
#[derive(Clone)]
enum FieldValue {
Float(f32),
Bool(bool),
Text(String),
Color(Vec4),
Index(usize),
}
fn apply_field_change(
editor_world: &mut EditorWorld,
world: &mut World,
entity: Entity,
field: InspectorField,
value: FieldValue,
) {
let mut transform_dirty = false;
match (field, &value) {
(InspectorField::Name, FieldValue::Text(text)) => {
world.core.set_name(entity, Name(text.clone()));
}
(
InspectorField::TranslationX
| InspectorField::TranslationY
| InspectorField::TranslationZ,
FieldValue::Float(v),
) => {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
match field {
InspectorField::TranslationX => transform.translation.x = *v,
InspectorField::TranslationY => transform.translation.y = *v,
InspectorField::TranslationZ => transform.translation.z = *v,
_ => unreachable!(),
}
transform_dirty = true;
}
}
(
InspectorField::EulerX | InspectorField::EulerY | InspectorField::EulerZ,
FieldValue::Float(degrees),
) => {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
let (mut roll, mut pitch, mut yaw) = quat_to_euler_xyz(&transform.rotation);
let radians = degrees.to_radians();
match field {
InspectorField::EulerX => roll = radians,
InspectorField::EulerY => pitch = radians,
InspectorField::EulerZ => yaw = radians,
_ => unreachable!(),
}
transform.rotation = euler_xyz_to_quat(roll, pitch, yaw);
transform_dirty = true;
}
}
(
InspectorField::ScaleX | InspectorField::ScaleY | InspectorField::ScaleZ,
FieldValue::Float(v),
) => {
if let Some(transform) = world.core.get_local_transform_mut(entity) {
match field {
InspectorField::ScaleX => transform.scale.x = *v,
InspectorField::ScaleY => transform.scale.y = *v,
InspectorField::ScaleZ => transform.scale.z = *v,
_ => unreachable!(),
}
transform_dirty = true;
}
}
(InspectorField::Visibility, FieldValue::Bool(visible)) => {
world
.core
.set_visibility(entity, Visibility { visible: *visible });
}
(InspectorField::CastsShadow, FieldValue::Bool(value)) => {
if *value {
world.core.add_components(entity, CASTS_SHADOW);
world.core.set_casts_shadow(entity, CastsShadow);
} else {
world.core.remove_components(entity, CASTS_SHADOW);
}
}
(InspectorField::LightIntensity, FieldValue::Float(intensity)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.intensity = *intensity;
}
}
(InspectorField::LightRange, FieldValue::Float(range)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.range = *range;
}
}
(InspectorField::LightCastsShadow, FieldValue::Bool(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.cast_shadows = *value;
}
}
(InspectorField::LightColor, FieldValue::Color(color)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.color = Vec3::new(color.x, color.y, color.z);
}
}
(InspectorField::LightType, FieldValue::Index(index)) => {
use nightshade::ecs::light::components::LightType;
if let Some(light) = world.core.get_light_mut(entity) {
light.light_type = match *index {
1 => LightType::Point,
2 => LightType::Spot,
_ => LightType::Directional,
};
}
}
(InspectorField::LightInnerCone, FieldValue::Float(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.inner_cone_angle = *value;
}
}
(InspectorField::LightOuterCone, FieldValue::Float(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.outer_cone_angle = *value;
}
}
(InspectorField::LightShadowBias, FieldValue::Float(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.shadow_bias = *value;
}
}
(InspectorField::LightShadowResolution, FieldValue::Float(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.shadow_resolution = (*value).clamp(0.0, 8192.0) as u32;
}
}
(InspectorField::LightShadowDistance, FieldValue::Float(value)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.shadow_distance = *value;
}
}
(InspectorField::LightCookieTexture, FieldValue::Text(text)) => {
if let Some(light) = world.core.get_light_mut(entity) {
light.cookie_texture = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
}
(InspectorField::AmbientLight, FieldValue::Color(color)) => {
world.resources.graphics.ambient_light = [color.x, color.y, color.z, color.w];
}
(InspectorField::BloomEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.bloom_enabled = *value;
}
(InspectorField::BloomIntensity, FieldValue::Float(value)) => {
world.resources.graphics.bloom_intensity = *value;
}
(InspectorField::BloomThreshold, FieldValue::Float(value)) => {
world.resources.graphics.bloom_threshold = *value;
}
(InspectorField::BloomKnee, FieldValue::Float(value)) => {
world.resources.graphics.bloom_knee = *value;
}
(InspectorField::BloomFilterRadius, FieldValue::Float(value)) => {
world.resources.graphics.bloom_filter_radius = *value;
}
(InspectorField::SsaoEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.ssao_enabled = *value;
}
(InspectorField::SsaoRadius, FieldValue::Float(value)) => {
world.resources.graphics.ssao_radius = *value;
}
(InspectorField::SsaoIntensity, FieldValue::Float(value)) => {
world.resources.graphics.ssao_intensity = *value;
}
(InspectorField::SsaoBias, FieldValue::Float(value)) => {
world.resources.graphics.ssao_bias = *value;
}
(InspectorField::SsaoSampleCount, FieldValue::Float(value)) => {
world.resources.graphics.ssao_sample_count = (*value).max(1.0) as u32;
}
(InspectorField::SsaoVisualization, FieldValue::Bool(value)) => {
world.resources.graphics.ssao_visualization = *value;
}
(InspectorField::SsgiEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.ssgi_enabled = *value;
}
(InspectorField::SsgiRadius, FieldValue::Float(value)) => {
world.resources.graphics.ssgi_radius = *value;
}
(InspectorField::SsgiIntensity, FieldValue::Float(value)) => {
world.resources.graphics.ssgi_intensity = *value;
}
(InspectorField::SsgiMaxSteps, FieldValue::Float(value)) => {
world.resources.graphics.ssgi_max_steps = (*value).max(1.0) as u32;
}
(InspectorField::SsrEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.ssr_enabled = *value;
}
(InspectorField::SsrMaxSteps, FieldValue::Float(value)) => {
world.resources.graphics.ssr_max_steps = (*value).max(1.0) as u32;
}
(InspectorField::SsrThickness, FieldValue::Float(value)) => {
world.resources.graphics.ssr_thickness = *value;
}
(InspectorField::SsrMaxDistance, FieldValue::Float(value)) => {
world.resources.graphics.ssr_max_distance = *value;
}
(InspectorField::SsrStride, FieldValue::Float(value)) => {
world.resources.graphics.ssr_stride = *value;
}
(InspectorField::SsrFadeStart, FieldValue::Float(value)) => {
world.resources.graphics.ssr_fade_start = *value;
}
(InspectorField::SsrFadeEnd, FieldValue::Float(value)) => {
world.resources.graphics.ssr_fade_end = *value;
}
(InspectorField::SsrIntensity, FieldValue::Float(value)) => {
world.resources.graphics.ssr_intensity = *value;
}
(InspectorField::FogEnabled, FieldValue::Bool(value)) => {
if *value {
if world.resources.graphics.fog.is_none() {
world.resources.graphics.fog =
Some(nightshade::ecs::graphics::resources::Fog::default());
}
} else {
world.resources.graphics.fog = None;
}
editor_world.resources.ui_handles.inspector.dirty = true;
}
(InspectorField::FogColor, FieldValue::Color(color)) => {
if let Some(fog) = world.resources.graphics.fog.as_mut() {
fog.color = [color.x, color.y, color.z];
}
}
(InspectorField::FogStart, FieldValue::Float(value)) => {
if let Some(fog) = world.resources.graphics.fog.as_mut() {
fog.start = *value;
}
}
(InspectorField::FogEnd, FieldValue::Float(value)) => {
if let Some(fog) = world.resources.graphics.fog.as_mut() {
fog.end = *value;
}
}
(InspectorField::DofEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.depth_of_field.enabled = *value;
}
(InspectorField::DofFocusDistance, FieldValue::Float(value)) => {
world.resources.graphics.depth_of_field.focus_distance = *value;
}
(InspectorField::DofFocusRange, FieldValue::Float(value)) => {
world.resources.graphics.depth_of_field.focus_range = *value;
}
(InspectorField::DofMaxBlurRadius, FieldValue::Float(value)) => {
world.resources.graphics.depth_of_field.max_blur_radius = *value;
}
(InspectorField::DofBokehThreshold, FieldValue::Float(value)) => {
world.resources.graphics.depth_of_field.bokeh_threshold = *value;
}
(InspectorField::DofBokehIntensity, FieldValue::Float(value)) => {
world.resources.graphics.depth_of_field.bokeh_intensity = *value;
}
(InspectorField::DofQuality, FieldValue::Index(index)) => {
use nightshade::ecs::graphics::resources::DepthOfFieldQuality;
world.resources.graphics.depth_of_field.quality = match *index {
0 => DepthOfFieldQuality::Low,
1 => DepthOfFieldQuality::Medium,
_ => DepthOfFieldQuality::High,
};
}
(InspectorField::FxaaEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.fxaa_enabled = *value;
}
(InspectorField::RenderScale, FieldValue::Float(value)) => {
world.resources.graphics.render_scale = (*value).clamp(0.25, 4.0);
}
(InspectorField::IblBlendFactor, FieldValue::Float(value)) => {
world.resources.graphics.ibl_blend_factor = (*value).clamp(0.0, 1.0);
}
(InspectorField::PbrDebug, FieldValue::Index(index)) => {
use nightshade::ecs::graphics::resources::PbrDebugMode;
world.resources.graphics.pbr_debug_mode = PbrDebugMode::ALL
.get(*index)
.copied()
.unwrap_or(PbrDebugMode::None);
}
(InspectorField::UnlitMode, FieldValue::Bool(value)) => {
world.resources.graphics.unlit_mode = *value;
}
(InspectorField::SelectionOutlineEnabled, FieldValue::Bool(value)) => {
world.resources.graphics.selection_outline_enabled = *value;
}
(InspectorField::SelectionOutlineColor, FieldValue::Color(color)) => {
world.resources.graphics.selection_outline_color = [color.x, color.y, color.z, color.w];
}
(InspectorField::Exposure, FieldValue::Float(value)) => {
world.resources.graphics.color_grading.exposure = *value;
}
(InspectorField::Saturation, FieldValue::Float(value)) => {
world.resources.graphics.color_grading.saturation = *value;
}
(InspectorField::Contrast, FieldValue::Float(value)) => {
world.resources.graphics.color_grading.contrast = *value;
}
(InspectorField::Brightness, FieldValue::Float(value)) => {
world.resources.graphics.color_grading.brightness = *value;
}
(InspectorField::Gamma, FieldValue::Float(value)) => {
world.resources.graphics.color_grading.gamma = *value;
}
(InspectorField::Particle(field), value) => {
apply_particle_change(world, entity, field, value);
}
(InspectorField::Decal(field), value) => {
apply_decal_change(world, entity, field, value);
}
(InspectorField::Audio(field), value) => {
apply_audio_change(world, entity, field, value);
}
(InspectorField::RigidBody(field), value) => {
apply_rigid_body_change(world, entity, field, value);
}
(InspectorField::Collider(field), value) => {
apply_collider_change(world, entity, field, value);
}
(InspectorField::Navmesh(field), value) => {
apply_navmesh_change(world, entity, field, value);
}
(InspectorField::Camera(field), value) => {
apply_camera_change(world, entity, field, value);
}
(InspectorField::Animation(field), value) => {
apply_animation_change(world, entity, field, value);
}
(InspectorField::Material(field), value) => {
apply_material_change(world, entity, field, value);
}
(InspectorField::Text(field), value) => {
apply_text_change(world, entity, field, value);
}
(InspectorField::Grass(field), value) => {
apply_grass_change(world, entity, field, value);
}
(InspectorField::GrassInteractor(field), value) => {
apply_grass_interactor_change(world, entity, field, value);
}
(InspectorField::Misc(field), value) => {
if !apply_misc_scene_change(editor_world, entity, field, value) {
apply_misc_change(world, entity, field, value);
} else {
editor_world.resources.ui_handles.inspector.dirty = true;
}
}
(InspectorField::AddComponent(kind), FieldValue::Bool(true)) => {
attach_default_component(world, entity, kind);
editor_world.resources.ui_handles.inspector.dirty = true;
}
(InspectorField::RemoveComponent(kind), FieldValue::Bool(true)) => {
remove_component(world, entity, kind);
editor_world.resources.ui_handles.inspector.dirty = true;
}
(InspectorField::CharacterController(field), value) => {
apply_character_controller_change(world, entity, field, value);
}
(InspectorField::Instance(field), value) => {
let needs_rebuild = apply_instanced_mesh_change(world, entity, field, value);
if needs_rebuild {
editor_world.resources.ui_handles.inspector.dirty = true;
}
}
_ => {}
}
if transform_dirty {
mark_local_transform_dirty(world, entity);
}
crate::scene_writeback::sync_entity(
&mut editor_world.resources.project,
&editor_world.resources.editor_scene,
world,
entity,
);
}
fn quat_to_euler_xyz(quat: &Quat) -> (f32, f32, f32) {
let sinr = 2.0 * (quat.w * quat.i + quat.j * quat.k);
let cosr = 1.0 - 2.0 * (quat.i * quat.i + quat.j * quat.j);
let roll = sinr.atan2(cosr);
let sinp = 2.0 * (quat.w * quat.j - quat.k * quat.i);
let pitch = if sinp.abs() >= 1.0 {
std::f32::consts::FRAC_PI_2.copysign(sinp)
} else {
sinp.asin()
};
let siny = 2.0 * (quat.w * quat.k + quat.i * quat.j);
let cosy = 1.0 - 2.0 * (quat.j * quat.j + quat.k * quat.k);
let yaw = siny.atan2(cosy);
(roll, pitch, yaw)
}
fn euler_xyz_to_quat(roll: f32, pitch: f32, yaw: f32) -> Quat {
let cr = (roll * 0.5).cos();
let sr = (roll * 0.5).sin();
let cp = (pitch * 0.5).cos();
let sp = (pitch * 0.5).sin();
let cy = (yaw * 0.5).cos();
let sy = (yaw * 0.5).sin();
Quat::from_parts(
cr * cp * cy + sr * sp * sy,
nalgebra_glm::Vec3::new(
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
),
)
}
pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
if !editor_world.resources.ui_handles.inspector.dirty {
return;
}
editor_world.resources.ui_handles.inspector.dirty = false;
let header_label = editor_world.resources.ui_handles.inspector.header_label;
let body_root = editor_world.resources.ui_handles.inspector.body_root;
for node in std::mem::take(&mut editor_world.resources.ui_handles.inspector.body_entities) {
despawn_recursive_immediate(world, node);
}
editor_world.resources.ui_handles.inspector.bindings.clear();
editor_world
.resources
.ui_handles
.inspector
.editing_state
.clear();
editor_world
.resources
.ui_handles
.inspector
.pre_drag_transforms
.clear();
editor_world
.resources
.ui_handles
.inspector
.pre_drag_components
.clear();
editor_world
.resources
.ui_handles
.inspector
.pending_picker_edits
.clear();
editor_world
.resources
.ui_handles
.inspector
.tag_remove_buttons
.clear();
editor_world.resources.ui_handles.inspector.tag_input = None;
let selected = editor_world.resources.ui.selected_entity;
let header_text = match selected {
Some(entity) => world
.core
.get_name(entity)
.map(|n| n.0.clone())
.filter(|n| !n.is_empty())
.unwrap_or_else(|| format!("Entity {}", entity.id)),
None => "World".to_string(),
};
ui_set_text(world, header_label, &header_text);
let scene_snapshot = SceneStructureSnapshot {
layers: editor_world.resources.project.scene.layers.clone(),
chunks: editor_world.resources.project.scene.chunks.clone(),
entity_layer: selected
.and_then(|entity| editor_world.resources.editor_scene.entity_layer(entity)),
entity_chunk: selected
.and_then(|entity| editor_world.resources.editor_scene.entity_chunk(entity)),
joints: editor_world.resources.project.scene.joints.clone(),
};
let mut new_nodes: Vec<Entity> = Vec::new();
let mut bindings: HashMap<Entity, InspectorBinding> = HashMap::new();
let mut tag_remove_buttons: HashMap<Entity, String> = HashMap::new();
let mut tag_input: Option<Entity> = None;
{
let mut tree = UiTreeBuilder::new(world);
let snapshot_ref = &scene_snapshot;
tree.in_parent(body_root, |tree| match selected {
Some(entity) => populate_entity_sections(
tree,
entity,
&mut new_nodes,
&mut bindings,
&mut tag_remove_buttons,
&mut tag_input,
snapshot_ref,
),
None => populate_world_sections(tree, &mut new_nodes, &mut bindings, snapshot_ref),
});
tree.finish();
}
editor_world.resources.ui_handles.inspector.body_entities = new_nodes;
editor_world.resources.ui_handles.inspector.bindings = bindings;
editor_world
.resources
.ui_handles
.inspector
.tag_remove_buttons = tag_remove_buttons;
editor_world.resources.ui_handles.inspector.tag_input = tag_input;
}
fn populate_entity_sections(
tree: &mut UiTreeBuilder,
entity: Entity,
new_nodes: &mut Vec<Entity>,
bindings: &mut HashMap<Entity, InspectorBinding>,
tag_remove_buttons: &mut HashMap<Entity, String>,
tag_input: &mut Option<Entity>,
scene_snapshot: &SceneStructureSnapshot,
) {
let grid = tree.add_property_grid(96.0);
new_nodes.push(grid);
let identity_section = tree.add_property_section(grid, "Identity");
add_property_value(tree, grid, identity_section, "ID", &entity.id.to_string());
let current_name = tree
.world_mut()
.core
.get_name(entity)
.map(|n| n.0.clone())
.unwrap_or_default();
let name_input = add_property_text_input(
tree,
grid,
identity_section,
"Name",
¤t_name,
"Entity name",
);
bindings.insert(
name_input,
InspectorBinding {
entity,
field: InspectorField::Name,
},
);
let visible = tree
.world_mut()
.core
.get_visibility(entity)
.map(|v| v.visible)
.unwrap_or(true);
let visibility_checkbox =
add_property_checkbox(tree, grid, identity_section, "Visible", visible);
bindings.insert(
visibility_checkbox,
InspectorBinding {
entity,
field: InspectorField::Visibility,
},
);
let casts_shadow = tree.world_mut().core.entity_has_casts_shadow(entity);
let casts_checkbox =
add_property_checkbox(tree, grid, identity_section, "Casts shadow", casts_shadow);
bindings.insert(
casts_checkbox,
InspectorBinding {
entity,
field: InspectorField::CastsShadow,
},
);
let tags_snapshot: Vec<String> = tree
.world_mut()
.resources
.entities
.tags
.get(&entity)
.cloned()
.unwrap_or_default();
let tags_section = tree.add_property_section(grid, "Tags");
if tags_snapshot.is_empty() {
add_property_value(tree, grid, tags_section, "Tags", "(none)");
} else {
for tag in &tags_snapshot {
let row = tree.add_property_row(grid, tags_section, tag);
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size * 0.85;
tree.in_parent(row, |tree| {
let button = tree
.add_node()
.size((22.0).px(), (22.0).px())
.with_text("\u{00d7}", font)
.text_center()
.color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
.color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.entity();
tag_remove_buttons.insert(button, tag.clone());
});
}
}
let add_row = tree.add_property_row(grid, tags_section, "Add tag");
tree.in_parent(add_row, |tree| {
let input = tree.add_text_input("tag name");
*tag_input = Some(input);
});
if let Some(transform) = tree.world_mut().core.get_local_transform(entity).copied() {
let section = tree.add_property_section(grid, "Transform");
let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);
let tx = add_property_drag_value(
tree,
grid,
section,
"Translation X",
transform.translation.x,
0.05,
2,
);
bindings.insert(
tx,
InspectorBinding {
entity,
field: InspectorField::TranslationX,
},
);
let ty = add_property_drag_value(
tree,
grid,
section,
"Translation Y",
transform.translation.y,
0.05,
2,
);
bindings.insert(
ty,
InspectorBinding {
entity,
field: InspectorField::TranslationY,
},
);
let tz = add_property_drag_value(
tree,
grid,
section,
"Translation Z",
transform.translation.z,
0.05,
2,
);
bindings.insert(
tz,
InspectorBinding {
entity,
field: InspectorField::TranslationZ,
},
);
let rx = add_property_drag_value(
tree,
grid,
section,
"Rotation X (\u{00b0})",
roll.to_degrees(),
0.5,
1,
);
bindings.insert(
rx,
InspectorBinding {
entity,
field: InspectorField::EulerX,
},
);
let ry = add_property_drag_value(
tree,
grid,
section,
"Rotation Y (\u{00b0})",
pitch.to_degrees(),
0.5,
1,
);
bindings.insert(
ry,
InspectorBinding {
entity,
field: InspectorField::EulerY,
},
);
let rz = add_property_drag_value(
tree,
grid,
section,
"Rotation Z (\u{00b0})",
yaw.to_degrees(),
0.5,
1,
);
bindings.insert(
rz,
InspectorBinding {
entity,
field: InspectorField::EulerZ,
},
);
let sx =
add_property_drag_value(tree, grid, section, "Scale X", transform.scale.x, 0.01, 2);
bindings.insert(
sx,
InspectorBinding {
entity,
field: InspectorField::ScaleX,
},
);
let sy =
add_property_drag_value(tree, grid, section, "Scale Y", transform.scale.y, 0.01, 2);
bindings.insert(
sy,
InspectorBinding {
entity,
field: InspectorField::ScaleY,
},
);
let sz =
add_property_drag_value(tree, grid, section, "Scale Z", transform.scale.z, 0.01, 2);
bindings.insert(
sz,
InspectorBinding {
entity,
field: InspectorField::ScaleZ,
},
);
}
if let Some(light) = tree.world_mut().core.get_light(entity).cloned() {
use nightshade::ecs::light::components::LightType;
let section = tree.add_property_section(grid, "Light");
let type_index = match light.light_type {
LightType::Directional => 0,
LightType::Point => 1,
LightType::Spot => 2,
};
let type_dropdown = add_property_dropdown(
tree,
grid,
section,
"Type",
&["Directional", "Point", "Spot"],
type_index,
);
bindings.insert(
type_dropdown,
InspectorBinding {
entity,
field: InspectorField::LightType,
},
);
let intensity =
add_property_drag_value(tree, grid, section, "Intensity", light.intensity, 0.05, 2);
bindings.insert(
intensity,
InspectorBinding {
entity,
field: InspectorField::LightIntensity,
},
);
let range = add_property_drag_value(tree, grid, section, "Range", light.range, 0.1, 2);
bindings.insert(
range,
InspectorBinding {
entity,
field: InspectorField::LightRange,
},
);
let color_picker = add_property_color_picker(
tree,
grid,
section,
"Color",
vec4(light.color.x, light.color.y, light.color.z, 1.0),
);
bindings.insert(
color_picker,
InspectorBinding {
entity,
field: InspectorField::LightColor,
},
);
if matches!(light.light_type, LightType::Spot) {
let inner = add_property_drag_value(
tree,
grid,
section,
"Inner cone (\u{00b0})",
light.inner_cone_angle.to_degrees(),
0.5,
1,
);
bindings.insert(
inner,
InspectorBinding {
entity,
field: InspectorField::LightInnerCone,
},
);
let outer = add_property_drag_value(
tree,
grid,
section,
"Outer cone (\u{00b0})",
light.outer_cone_angle.to_degrees(),
0.5,
1,
);
bindings.insert(
outer,
InspectorBinding {
entity,
field: InspectorField::LightOuterCone,
},
);
}
let cast = add_property_checkbox(tree, grid, section, "Cast shadows", light.cast_shadows);
bindings.insert(
cast,
InspectorBinding {
entity,
field: InspectorField::LightCastsShadow,
},
);
let shadow_bias = add_property_drag_value(
tree,
grid,
section,
"Shadow bias",
light.shadow_bias,
0.0001,
5,
);
bindings.insert(
shadow_bias,
InspectorBinding {
entity,
field: InspectorField::LightShadowBias,
},
);
let shadow_resolution = add_property_drag_value(
tree,
grid,
section,
"Shadow resolution",
light.shadow_resolution as f32,
32.0,
0,
);
bindings.insert(
shadow_resolution,
InspectorBinding {
entity,
field: InspectorField::LightShadowResolution,
},
);
let shadow_distance = add_property_drag_value(
tree,
grid,
section,
"Shadow distance",
light.shadow_distance,
0.5,
2,
);
bindings.insert(
shadow_distance,
InspectorBinding {
entity,
field: InspectorField::LightShadowDistance,
},
);
let cookie_text = light.cookie_texture.clone().unwrap_or_default();
let cookie = add_property_text_input(
tree,
grid,
section,
"Cookie texture",
&cookie_text,
"path/to/cookie",
);
bindings.insert(
cookie,
InspectorBinding {
entity,
field: InspectorField::LightCookieTexture,
},
);
}
populate_camera_section(tree, grid, entity, bindings);
if let Some(mesh) = tree.world_mut().core.get_render_mesh(entity).cloned() {
let section = tree.add_property_section(grid, "Mesh");
add_property_value(tree, grid, section, "Name", &mesh.name);
}
populate_instanced_mesh_section(tree, grid, entity, bindings);
populate_material_section(tree, grid, entity, bindings);
if let Some(volume) = tree.world_mut().core.get_bounding_volume(entity).copied() {
let half = volume.obb.half_extents;
let extents = format!("{:.2} x {:.2} x {:.2}", half.x, half.y, half.z);
let radius = format!("{:.2}", volume.sphere_radius);
let section = tree.add_property_section(grid, "Bounds");
add_property_value(tree, grid, section, "Half extents", &extents);
add_property_value(tree, grid, section, "Sphere radius", &radius);
}
populate_animation_section(tree, grid, entity, bindings);
populate_particle_section(tree, grid, entity, bindings);
populate_decal_section(tree, grid, entity, bindings);
populate_audio_section(tree, grid, entity, bindings);
populate_rigid_body_section(tree, grid, entity, bindings);
populate_collider_section(tree, grid, entity, bindings);
populate_character_controller_section(tree, grid, entity, bindings);
populate_navmesh_section(tree, grid, entity, bindings);
populate_text_section(tree, grid, entity, bindings);
populate_grass_section(tree, grid, entity, bindings);
populate_grass_interactor_section(tree, grid, entity, bindings);
populate_misc_section(tree, grid, entity, bindings, scene_snapshot);
populate_remove_component_section(tree, grid, entity, bindings);
populate_add_component_section(tree, grid, entity, bindings);
}
fn populate_world_sections(
tree: &mut UiTreeBuilder,
new_nodes: &mut Vec<Entity>,
bindings: &mut HashMap<Entity, InspectorBinding>,
scene_snapshot: &SceneStructureSnapshot,
) {
let grid = tree.add_property_grid(120.0);
new_nodes.push(grid);
let graphics = tree.world_mut().resources.graphics.clone();
let lighting_section = tree.add_property_section(grid, "Lighting");
let ambient_widget = add_property_color_picker(
tree,
grid,
lighting_section,
"Ambient",
vec4(
graphics.ambient_light[0],
graphics.ambient_light[1],
graphics.ambient_light[2],
graphics.ambient_light[3],
),
);
bindings.insert(
ambient_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::AmbientLight,
},
);
let bloom_section = tree.add_property_section(grid, "Bloom");
let bloom_enabled =
add_property_checkbox(tree, grid, bloom_section, "Enabled", graphics.bloom_enabled);
bindings.insert(
bloom_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::BloomEnabled,
},
);
let bloom_intensity = add_property_drag_value(
tree,
grid,
bloom_section,
"Intensity",
graphics.bloom_intensity,
0.01,
3,
);
bindings.insert(
bloom_intensity,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::BloomIntensity,
},
);
let bloom_threshold = add_property_drag_value(
tree,
grid,
bloom_section,
"Threshold",
graphics.bloom_threshold,
0.05,
2,
);
bindings.insert(
bloom_threshold,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::BloomThreshold,
},
);
let bloom_knee = add_property_drag_value(
tree,
grid,
bloom_section,
"Knee",
graphics.bloom_knee,
0.01,
3,
);
bindings.insert(
bloom_knee,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::BloomKnee,
},
);
let bloom_filter_radius = add_property_drag_value(
tree,
grid,
bloom_section,
"Filter Radius",
graphics.bloom_filter_radius,
0.001,
4,
);
bindings.insert(
bloom_filter_radius,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::BloomFilterRadius,
},
);
let ssao_section = tree.add_property_section(grid, "SSAO");
let ssao_enabled =
add_property_checkbox(tree, grid, ssao_section, "Enabled", graphics.ssao_enabled);
bindings.insert(
ssao_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoEnabled,
},
);
let ssao_radius = add_property_drag_value(
tree,
grid,
ssao_section,
"Radius",
graphics.ssao_radius,
0.05,
2,
);
bindings.insert(
ssao_radius,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoRadius,
},
);
let ssao_intensity = add_property_drag_value(
tree,
grid,
ssao_section,
"Intensity",
graphics.ssao_intensity,
0.05,
2,
);
bindings.insert(
ssao_intensity,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoIntensity,
},
);
let ssao_bias = add_property_drag_value(
tree,
grid,
ssao_section,
"Bias",
graphics.ssao_bias,
0.001,
4,
);
bindings.insert(
ssao_bias,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoBias,
},
);
let ssao_samples = add_property_drag_value(
tree,
grid,
ssao_section,
"Samples",
graphics.ssao_sample_count as f32,
1.0,
0,
);
bindings.insert(
ssao_samples,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoSampleCount,
},
);
let ssao_visualization = add_property_checkbox(
tree,
grid,
ssao_section,
"Visualize",
graphics.ssao_visualization,
);
bindings.insert(
ssao_visualization,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsaoVisualization,
},
);
let ssgi_section = tree.add_property_section(grid, "SSGI");
let ssgi_enabled =
add_property_checkbox(tree, grid, ssgi_section, "Enabled", graphics.ssgi_enabled);
bindings.insert(
ssgi_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsgiEnabled,
},
);
let ssgi_radius = add_property_drag_value(
tree,
grid,
ssgi_section,
"Radius",
graphics.ssgi_radius,
0.1,
2,
);
bindings.insert(
ssgi_radius,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsgiRadius,
},
);
let ssgi_intensity = add_property_drag_value(
tree,
grid,
ssgi_section,
"Intensity",
graphics.ssgi_intensity,
0.05,
2,
);
bindings.insert(
ssgi_intensity,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsgiIntensity,
},
);
let ssgi_max_steps = add_property_drag_value(
tree,
grid,
ssgi_section,
"Max Steps",
graphics.ssgi_max_steps as f32,
1.0,
0,
);
bindings.insert(
ssgi_max_steps,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsgiMaxSteps,
},
);
let ssr_section = tree.add_property_section(grid, "SSR");
let ssr_enabled =
add_property_checkbox(tree, grid, ssr_section, "Enabled", graphics.ssr_enabled);
bindings.insert(
ssr_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrEnabled,
},
);
let ssr_max_steps = add_property_drag_value(
tree,
grid,
ssr_section,
"Max Steps",
graphics.ssr_max_steps as f32,
1.0,
0,
);
bindings.insert(
ssr_max_steps,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrMaxSteps,
},
);
let ssr_thickness = add_property_drag_value(
tree,
grid,
ssr_section,
"Thickness",
graphics.ssr_thickness,
0.01,
3,
);
bindings.insert(
ssr_thickness,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrThickness,
},
);
let ssr_max_distance = add_property_drag_value(
tree,
grid,
ssr_section,
"Max Distance",
graphics.ssr_max_distance,
0.5,
2,
);
bindings.insert(
ssr_max_distance,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrMaxDistance,
},
);
let ssr_stride = add_property_drag_value(
tree,
grid,
ssr_section,
"Stride",
graphics.ssr_stride,
0.05,
2,
);
bindings.insert(
ssr_stride,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrStride,
},
);
let ssr_fade_start = add_property_drag_value(
tree,
grid,
ssr_section,
"Fade Start",
graphics.ssr_fade_start,
0.01,
2,
);
bindings.insert(
ssr_fade_start,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrFadeStart,
},
);
let ssr_fade_end = add_property_drag_value(
tree,
grid,
ssr_section,
"Fade End",
graphics.ssr_fade_end,
0.01,
2,
);
bindings.insert(
ssr_fade_end,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrFadeEnd,
},
);
let ssr_intensity = add_property_drag_value(
tree,
grid,
ssr_section,
"Intensity",
graphics.ssr_intensity,
0.05,
2,
);
bindings.insert(
ssr_intensity,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SsrIntensity,
},
);
let fog_section = tree.add_property_section(grid, "Fog");
let fog_enabled =
add_property_checkbox(tree, grid, fog_section, "Enabled", graphics.fog.is_some());
bindings.insert(
fog_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::FogEnabled,
},
);
if let Some(fog) = graphics.fog.as_ref() {
let fog_color = add_property_color_picker(
tree,
grid,
fog_section,
"Color",
vec4(fog.color[0], fog.color[1], fog.color[2], 1.0),
);
bindings.insert(
fog_color,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::FogColor,
},
);
let fog_start =
add_property_drag_value(tree, grid, fog_section, "Start", fog.start, 0.1, 2);
bindings.insert(
fog_start,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::FogStart,
},
);
let fog_end = add_property_drag_value(tree, grid, fog_section, "End", fog.end, 0.1, 2);
bindings.insert(
fog_end,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::FogEnd,
},
);
}
let dof_section = tree.add_property_section(grid, "Depth of Field");
let dof_enabled = add_property_checkbox(
tree,
grid,
dof_section,
"Enabled",
graphics.depth_of_field.enabled,
);
bindings.insert(
dof_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofEnabled,
},
);
let dof_focus_distance = add_property_drag_value(
tree,
grid,
dof_section,
"Focus Distance",
graphics.depth_of_field.focus_distance,
0.1,
2,
);
bindings.insert(
dof_focus_distance,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofFocusDistance,
},
);
let dof_focus_range = add_property_drag_value(
tree,
grid,
dof_section,
"Focus Range",
graphics.depth_of_field.focus_range,
0.1,
2,
);
bindings.insert(
dof_focus_range,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofFocusRange,
},
);
let dof_blur_radius = add_property_drag_value(
tree,
grid,
dof_section,
"Max Blur",
graphics.depth_of_field.max_blur_radius,
0.1,
2,
);
bindings.insert(
dof_blur_radius,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofMaxBlurRadius,
},
);
let dof_bokeh_threshold = add_property_drag_value(
tree,
grid,
dof_section,
"Bokeh Threshold",
graphics.depth_of_field.bokeh_threshold,
0.05,
2,
);
bindings.insert(
dof_bokeh_threshold,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofBokehThreshold,
},
);
let dof_bokeh_intensity = add_property_drag_value(
tree,
grid,
dof_section,
"Bokeh Intensity",
graphics.depth_of_field.bokeh_intensity,
0.05,
2,
);
bindings.insert(
dof_bokeh_intensity,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofBokehIntensity,
},
);
let dof_quality_index = match graphics.depth_of_field.quality {
nightshade::ecs::graphics::resources::DepthOfFieldQuality::Low => 0,
nightshade::ecs::graphics::resources::DepthOfFieldQuality::Medium => 1,
nightshade::ecs::graphics::resources::DepthOfFieldQuality::High => 2,
};
let dof_quality = add_property_dropdown(
tree,
grid,
dof_section,
"Quality",
&["Low", "Medium", "High"],
dof_quality_index,
);
bindings.insert(
dof_quality,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::DofQuality,
},
);
let post_section = tree.add_property_section(grid, "Post Processing");
let fxaa = add_property_checkbox(tree, grid, post_section, "FXAA", graphics.fxaa_enabled);
bindings.insert(
fxaa,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::FxaaEnabled,
},
);
let render_scale = add_property_drag_value(
tree,
grid,
post_section,
"Render Scale",
graphics.render_scale,
0.05,
2,
);
bindings.insert(
render_scale,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::RenderScale,
},
);
let ibl_blend = add_property_drag_value(
tree,
grid,
post_section,
"IBL Blend",
graphics.ibl_blend_factor,
0.01,
2,
);
bindings.insert(
ibl_blend,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::IblBlendFactor,
},
);
let unlit = add_property_checkbox(tree, grid, post_section, "Unlit", graphics.unlit_mode);
bindings.insert(
unlit,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::UnlitMode,
},
);
let pbr_options: Vec<&str> = nightshade::ecs::graphics::resources::PbrDebugMode::ALL
.iter()
.map(|mode| mode.name())
.collect();
let pbr_index = nightshade::ecs::graphics::resources::PbrDebugMode::ALL
.iter()
.position(|mode| *mode == graphics.pbr_debug_mode)
.unwrap_or(0);
let pbr_dropdown = add_property_dropdown(
tree,
grid,
post_section,
"PBR Debug",
&pbr_options,
pbr_index,
);
bindings.insert(
pbr_dropdown,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::PbrDebug,
},
);
let outline_section = tree.add_property_section(grid, "Selection Outline");
let outline_enabled = add_property_checkbox(
tree,
grid,
outline_section,
"Enabled",
graphics.selection_outline_enabled,
);
bindings.insert(
outline_enabled,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SelectionOutlineEnabled,
},
);
let outline_color = add_property_color_picker(
tree,
grid,
outline_section,
"Color",
vec4(
graphics.selection_outline_color[0],
graphics.selection_outline_color[1],
graphics.selection_outline_color[2],
graphics.selection_outline_color[3],
),
);
bindings.insert(
outline_color,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::SelectionOutlineColor,
},
);
let grading_section = tree.add_property_section(grid, "Color Grading");
let exposure_widget = add_property_drag_value(
tree,
grid,
grading_section,
"Exposure",
graphics.color_grading.exposure,
0.01,
3,
);
bindings.insert(
exposure_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Exposure,
},
);
let saturation_widget = add_property_drag_value(
tree,
grid,
grading_section,
"Saturation",
graphics.color_grading.saturation,
0.01,
2,
);
bindings.insert(
saturation_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Saturation,
},
);
let contrast_widget = add_property_drag_value(
tree,
grid,
grading_section,
"Contrast",
graphics.color_grading.contrast,
0.01,
2,
);
bindings.insert(
contrast_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Contrast,
},
);
let brightness_widget = add_property_drag_value(
tree,
grid,
grading_section,
"Brightness",
graphics.color_grading.brightness,
0.01,
2,
);
bindings.insert(
brightness_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Brightness,
},
);
let gamma_widget = add_property_drag_value(
tree,
grid,
grading_section,
"Gamma",
graphics.color_grading.gamma,
0.01,
2,
);
bindings.insert(
gamma_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Gamma,
},
);
populate_scene_structure_section(tree, grid, bindings, scene_snapshot);
}
fn populate_scene_structure_section(
tree: &mut UiTreeBuilder,
grid: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
snapshot: &SceneStructureSnapshot,
) {
let layers_section = tree.add_property_section(grid, "Scene Layers");
let add_layer_widget = add_property_button(tree, grid, layers_section, "Layers", "Add layer");
bindings.insert(
add_layer_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneAddLayer),
},
);
for layer in &snapshot.layers {
let name_widget = add_property_text_input(
tree,
grid,
layers_section,
&format!("Layer {}", layer.id.0),
&layer.name,
"name",
);
bindings.insert(
name_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneLayerName(layer.id.0)),
},
);
let order_widget = add_property_drag_value(
tree,
grid,
layers_section,
"Order",
layer.order as f32,
1.0,
0,
);
bindings.insert(
order_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneLayerOrder(layer.id.0)),
},
);
let enabled_widget =
add_property_checkbox(tree, grid, layers_section, "Enabled", layer.enabled);
bindings.insert(
enabled_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneLayerEnabled(layer.id.0)),
},
);
let remove_widget = add_property_button(tree, grid, layers_section, "Remove", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneRemoveLayer(layer.id.0)),
},
);
}
let chunks_section = tree.add_property_section(grid, "Scene Chunks");
let add_chunk_widget = add_property_button(tree, grid, chunks_section, "Chunks", "Add chunk");
bindings.insert(
add_chunk_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneAddChunk),
},
);
for chunk in &snapshot.chunks {
let name_widget = add_property_text_input(
tree,
grid,
chunks_section,
&format!("Chunk {}", chunk.id.0),
&chunk.name,
"name",
);
bindings.insert(
name_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkName(chunk.id.0)),
},
);
let load = add_property_drag_value(
tree,
grid,
chunks_section,
"Load distance",
chunk.load_distance,
0.5,
2,
);
bindings.insert(
load,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkLoadDistance(chunk.id.0)),
},
);
let unload = add_property_drag_value(
tree,
grid,
chunks_section,
"Unload distance",
chunk.unload_distance,
0.5,
2,
);
bindings.insert(
unload,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkUnloadDistance(chunk.id.0)),
},
);
let min_x = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Min X",
chunk.bounds_min[0],
0.5,
2,
);
bindings.insert(
min_x,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMinX(chunk.id.0)),
},
);
let min_y = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Min Y",
chunk.bounds_min[1],
0.5,
2,
);
bindings.insert(
min_y,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMinY(chunk.id.0)),
},
);
let min_z = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Min Z",
chunk.bounds_min[2],
0.5,
2,
);
bindings.insert(
min_z,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMinZ(chunk.id.0)),
},
);
let max_x = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Max X",
chunk.bounds_max[0],
0.5,
2,
);
bindings.insert(
max_x,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxX(chunk.id.0)),
},
);
let max_y = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Max Y",
chunk.bounds_max[1],
0.5,
2,
);
bindings.insert(
max_y,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxY(chunk.id.0)),
},
);
let max_z = add_property_drag_value(
tree,
grid,
chunks_section,
"Bounds Max Z",
chunk.bounds_max[2],
0.5,
2,
);
bindings.insert(
max_z,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxZ(chunk.id.0)),
},
);
let remove_widget = add_property_button(tree, grid, chunks_section, "Remove", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::SceneRemoveChunk(chunk.id.0)),
},
);
}
let joints_section = tree.add_property_section(grid, "Joints");
add_property_value(
tree,
grid,
joints_section,
"Hint",
"Select two entities, then add a joint.",
);
let kinds: [(&str, MiscField); 6] = [
("Fixed", MiscField::AddFixedJoint),
("Revolute", MiscField::AddRevoluteJoint),
("Prismatic", MiscField::AddPrismaticJoint),
("Spherical", MiscField::AddSphericalJoint),
("Rope", MiscField::AddRopeJoint),
("Spring", MiscField::AddSpringJoint),
];
for (label, field) in kinds {
let widget = add_property_button(tree, grid, joints_section, label, "Add");
bindings.insert(
widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(field),
},
);
}
for (index, joint) in snapshot.joints.iter().enumerate() {
let label_text = format!(
"{} #{} parent={} child={}",
scene_joint_kind_name(&joint.joint),
index,
joint.parent_entity,
joint.child_entity
);
add_property_value(tree, grid, joints_section, "Joint", &label_text);
let collisions = add_property_checkbox(
tree,
grid,
joints_section,
"Collisions",
joint.collisions_enabled,
);
bindings.insert(
collisions,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::ToggleJointCollisions(index)),
},
);
let remove_widget = add_property_button(tree, grid, joints_section, "Remove", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity: Entity::default(),
field: InspectorField::Misc(MiscField::RemoveJoint(index)),
},
);
}
}
fn scene_joint_kind_name(joint: &nightshade::ecs::scene::SceneJoint) -> &'static str {
use nightshade::ecs::scene::SceneJoint;
match joint {
SceneJoint::Fixed { .. } => "Fixed",
SceneJoint::Revolute { .. } => "Revolute",
SceneJoint::Prismatic { .. } => "Prismatic",
SceneJoint::Spherical { .. } => "Spherical",
SceneJoint::Rope { .. } => "Rope",
SceneJoint::Spring { .. } => "Spring",
}
}
fn add_property_value(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
value: &str,
) {
let row = tree.add_property_row(grid, section, label);
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size * 0.9;
let color = theme.text_color;
tree.in_parent(row, |tree| {
tree.add_node()
.size(100.pct(), (22.0).px())
.flex_grow(1.0)
.with_text(value, font)
.text_left()
.color_raw::<UiBase>(color)
.entity();
});
}
fn add_property_drag_value(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
initial: f32,
speed: f32,
precision: usize,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_drag_value_configured(
DragValueConfig::new(f32::MIN, f32::MAX, initial)
.speed(speed)
.precision(precision),
);
});
widget
}
fn add_property_checkbox(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
initial: bool,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_checkbox("", initial);
});
widget
}
fn add_property_text_input(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
initial: &str,
placeholder: &str,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_text_input_with_value(placeholder, initial);
});
widget
}
fn add_property_color_picker(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
initial: Vec4,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_color_picker(initial);
});
widget
}
fn add_property_dropdown(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
options: &[&str],
initial: usize,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
tree.in_parent(row, |tree| {
widget = tree.add_dropdown(options, initial);
});
widget
}
fn add_property_button(
tree: &mut UiTreeBuilder,
grid: Entity,
section: Entity,
label: &str,
button_text: &str,
) -> Entity {
let row = tree.add_property_row(grid, section, label);
let mut widget = Entity::default();
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size * 0.85;
tree.in_parent(row, |tree| {
widget = tree
.add_node()
.size(100.pct(), (24.0).px())
.with_text(button_text, font)
.text_center()
.color_raw::<UiBase>(vec4(0.75, 0.75, 0.8, 1.0))
.color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.entity();
});
widget
}
fn populate_camera_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(camera) = tree.world_mut().core.get_camera(entity).copied() else {
return;
};
let section = tree.add_property_section(grid, "Camera");
let active = tree.world_mut().resources.active_camera == Some(entity);
add_property_value(
tree,
grid,
section,
"Active",
if active { "yes" } else { "no" },
);
let activate_button =
add_property_button(tree, grid, section, "Activate", "Make active camera");
bindings.insert(
activate_button,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::SetActive),
},
);
let kind_index = match camera.projection {
nightshade::ecs::camera::components::Projection::Perspective(_) => 0,
nightshade::ecs::camera::components::Projection::Orthographic(_) => 1,
};
let kind_widget = add_property_dropdown(
tree,
grid,
section,
"Projection",
&["Perspective", "Orthographic"],
kind_index,
);
bindings.insert(
kind_widget,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::ProjectionKind),
},
);
match camera.projection {
nightshade::ecs::camera::components::Projection::Perspective(perspective) => {
let fov = add_property_drag_value(
tree,
grid,
section,
"FOV (\u{00b0})",
perspective.y_fov_rad.to_degrees(),
0.5,
1,
);
bindings.insert(
fov,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::FovDegrees),
},
);
let near =
add_property_drag_value(tree, grid, section, "Near", perspective.z_near, 0.001, 4);
bindings.insert(
near,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::NearPlane),
},
);
let far_override = add_property_checkbox(
tree,
grid,
section,
"Far override",
perspective.z_far.is_some(),
);
bindings.insert(
far_override,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::FarOverride),
},
);
let far_value = add_property_drag_value(
tree,
grid,
section,
"Far",
perspective.z_far.unwrap_or(1000.0),
0.5,
2,
);
bindings.insert(
far_value,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::FarValue),
},
);
let aspect_override = add_property_checkbox(
tree,
grid,
section,
"Aspect override",
perspective.aspect_ratio.is_some(),
);
bindings.insert(
aspect_override,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::AspectOverride),
},
);
let aspect_value = add_property_drag_value(
tree,
grid,
section,
"Aspect",
perspective.aspect_ratio.unwrap_or(16.0 / 9.0),
0.01,
3,
);
bindings.insert(
aspect_value,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::AspectValue),
},
);
}
nightshade::ecs::camera::components::Projection::Orthographic(ortho) => {
let x_mag = add_property_drag_value(tree, grid, section, "X mag", ortho.x_mag, 0.05, 2);
bindings.insert(
x_mag,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::OrthoX),
},
);
let y_mag = add_property_drag_value(tree, grid, section, "Y mag", ortho.y_mag, 0.05, 2);
bindings.insert(
y_mag,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::OrthoY),
},
);
let near = add_property_drag_value(tree, grid, section, "Near", ortho.z_near, 0.001, 4);
bindings.insert(
near,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::OrthoNear),
},
);
let far = add_property_drag_value(tree, grid, section, "Far", ortho.z_far, 0.5, 2);
bindings.insert(
far,
InspectorBinding {
entity,
field: InspectorField::Camera(CameraField::OrthoFar),
},
);
}
}
}
fn populate_material_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(material_ref) = tree.world_mut().core.get_material_ref(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Material");
add_property_value(tree, grid, section, "Name", &material_ref.name);
let material = nightshade::ecs::material::resources::material_registry_iter(
&tree.world_mut().resources.assets.material_registry,
)
.find(|(name, _)| name.as_str() == material_ref.name.as_str())
.map(|(_, material)| material.clone());
let Some(material) = material else {
add_property_value(tree, grid, section, "Status", "(not in registry)");
return;
};
let base_color = add_property_color_picker(
tree,
grid,
section,
"Base color",
vec4(
material.base_color[0],
material.base_color[1],
material.base_color[2],
material.base_color[3],
),
);
bindings.insert(
base_color,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::BaseColor),
},
);
let metallic =
add_property_drag_value(tree, grid, section, "Metallic", material.metallic, 0.01, 3);
bindings.insert(
metallic,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Metallic),
},
);
let roughness = add_property_drag_value(
tree,
grid,
section,
"Roughness",
material.roughness,
0.01,
3,
);
bindings.insert(
roughness,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Roughness),
},
);
let emissive = add_property_color_picker(
tree,
grid,
section,
"Emissive",
vec4(
material.emissive_factor[0],
material.emissive_factor[1],
material.emissive_factor[2],
1.0,
),
);
bindings.insert(
emissive,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::EmissiveFactor),
},
);
let emissive_strength = add_property_drag_value(
tree,
grid,
section,
"Emissive strength",
material.emissive_strength,
0.05,
2,
);
bindings.insert(
emissive_strength,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::EmissiveStrength),
},
);
let alpha_index = match material.alpha_mode {
nightshade::ecs::material::components::AlphaMode::Opaque => 0,
nightshade::ecs::material::components::AlphaMode::Mask => 1,
nightshade::ecs::material::components::AlphaMode::Blend => 2,
};
let alpha_mode = add_property_dropdown(
tree,
grid,
section,
"Alpha mode",
&["Opaque", "Mask", "Blend"],
alpha_index,
);
bindings.insert(
alpha_mode,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::AlphaMode),
},
);
let alpha_cutoff = add_property_drag_value(
tree,
grid,
section,
"Alpha cutoff",
material.alpha_cutoff,
0.01,
3,
);
bindings.insert(
alpha_cutoff,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::AlphaCutoff),
},
);
let unlit = add_property_checkbox(tree, grid, section, "Unlit", material.unlit);
bindings.insert(
unlit,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Unlit),
},
);
let double_sided =
add_property_checkbox(tree, grid, section, "Double sided", material.double_sided);
bindings.insert(
double_sided,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::DoubleSided),
},
);
let normal_scale = add_property_drag_value(
tree,
grid,
section,
"Normal scale",
material.normal_scale,
0.01,
3,
);
bindings.insert(
normal_scale,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::NormalScale),
},
);
let occlusion = add_property_drag_value(
tree,
grid,
section,
"Occlusion",
material.occlusion_strength,
0.01,
3,
);
bindings.insert(
occlusion,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::OcclusionStrength),
},
);
let ior = add_property_drag_value(tree, grid, section, "IOR", material.ior, 0.01, 3);
bindings.insert(
ior,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Ior),
},
);
let transmission = add_property_drag_value(
tree,
grid,
section,
"Transmission",
material.transmission_factor,
0.01,
3,
);
bindings.insert(
transmission,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Transmission),
},
);
let thickness = add_property_drag_value(
tree,
grid,
section,
"Thickness",
material.thickness,
0.05,
3,
);
bindings.insert(
thickness,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::Thickness),
},
);
let clearcoat = add_property_drag_value(
tree,
grid,
section,
"Clearcoat",
material.clearcoat_factor,
0.01,
3,
);
bindings.insert(
clearcoat,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::ClearcoatFactor),
},
);
let clearcoat_roughness = add_property_drag_value(
tree,
grid,
section,
"Clearcoat rough",
material.clearcoat_roughness_factor,
0.01,
3,
);
bindings.insert(
clearcoat_roughness,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::ClearcoatRoughness),
},
);
let sheen_color = add_property_color_picker(
tree,
grid,
section,
"Sheen color",
vec4(
material.sheen_color_factor[0],
material.sheen_color_factor[1],
material.sheen_color_factor[2],
1.0,
),
);
bindings.insert(
sheen_color,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::SheenColor),
},
);
let sheen_roughness = add_property_drag_value(
tree,
grid,
section,
"Sheen rough",
material.sheen_roughness_factor,
0.01,
3,
);
bindings.insert(
sheen_roughness,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::SheenRoughness),
},
);
let iridescence = add_property_drag_value(
tree,
grid,
section,
"Iridescence",
material.iridescence_factor,
0.01,
3,
);
bindings.insert(
iridescence,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::IridescenceFactor),
},
);
let anisotropy = add_property_drag_value(
tree,
grid,
section,
"Anisotropy",
material.anisotropy_strength,
0.01,
3,
);
bindings.insert(
anisotropy,
InspectorBinding {
entity,
field: InspectorField::Material(MaterialField::AnisotropyStrength),
},
);
}
fn populate_animation_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(player) = tree.world_mut().core.get_animation_player(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Animation");
add_property_value(
tree,
grid,
section,
"Clips",
&player.clips.len().to_string(),
);
if player.clips.is_empty() {
return;
}
let names: Vec<String> = player
.clips
.iter()
.enumerate()
.map(|(index, clip)| {
if clip.name.is_empty() {
format!("Clip {index}")
} else {
clip.name.clone()
}
})
.collect();
let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
let initial = player.current_clip.unwrap_or(0).min(player.clips.len() - 1);
let dropdown = add_property_dropdown(tree, grid, section, "Clip", &name_refs, initial);
bindings.insert(
dropdown,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::ClipSelect),
},
);
let playing = add_property_checkbox(tree, grid, section, "Playing", player.playing);
bindings.insert(
playing,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::Playing),
},
);
let looping = add_property_checkbox(tree, grid, section, "Loop", player.looping);
bindings.insert(
looping,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::Looping),
},
);
let play_all = add_property_checkbox(tree, grid, section, "Play all", player.play_all);
bindings.insert(
play_all,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::PlayAll),
},
);
let speed = add_property_drag_value(tree, grid, section, "Speed", player.speed, 0.05, 2);
bindings.insert(
speed,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::Speed),
},
);
let time = add_property_drag_value(tree, grid, section, "Time", player.time, 0.01, 3);
bindings.insert(
time,
InspectorBinding {
entity,
field: InspectorField::Animation(AnimationField::Time),
},
);
}
fn populate_particle_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(emitter) = tree.world_mut().core.get_particle_emitter(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Particles");
let enabled = add_property_checkbox(tree, grid, section, "Enabled", emitter.enabled);
bindings.insert(
enabled,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::Enabled),
},
);
let type_index = match emitter.emitter_type {
nightshade::ecs::particles::components::EmitterType::Firework => 0,
nightshade::ecs::particles::components::EmitterType::Fire => 1,
nightshade::ecs::particles::components::EmitterType::Smoke => 2,
nightshade::ecs::particles::components::EmitterType::Sparks => 3,
nightshade::ecs::particles::components::EmitterType::Trail => 4,
};
let type_widget = add_property_dropdown(
tree,
grid,
section,
"Type",
&["Firework", "Fire", "Smoke", "Sparks", "Trail"],
type_index,
);
bindings.insert(
type_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::EmitterType),
},
);
let shape_index = match emitter.shape {
nightshade::ecs::particles::components::EmitterShape::Point => 0,
nightshade::ecs::particles::components::EmitterShape::Sphere { .. } => 1,
nightshade::ecs::particles::components::EmitterShape::Cone { .. } => 2,
nightshade::ecs::particles::components::EmitterShape::Box { .. } => 3,
};
let shape_widget = add_property_dropdown(
tree,
grid,
section,
"Shape",
&["Point", "Sphere", "Cone", "Box"],
shape_index,
);
bindings.insert(
shape_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::Shape),
},
);
match emitter.shape {
nightshade::ecs::particles::components::EmitterShape::Point => {}
nightshade::ecs::particles::components::EmitterShape::Sphere { radius } => {
let widget = add_property_drag_value(tree, grid, section, "Radius", radius, 0.05, 2);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeRadius),
},
);
}
nightshade::ecs::particles::components::EmitterShape::Cone { angle, height } => {
let angle_widget =
add_property_drag_value(tree, grid, section, "Cone angle (rad)", angle, 0.01, 3);
bindings.insert(
angle_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeAngle),
},
);
let height_widget =
add_property_drag_value(tree, grid, section, "Cone height", height, 0.05, 2);
bindings.insert(
height_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeHeight),
},
);
}
nightshade::ecs::particles::components::EmitterShape::Box { half_extents } => {
let x = add_property_drag_value(tree, grid, section, "Box X", half_extents.x, 0.05, 2);
bindings.insert(
x,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeBoxX),
},
);
let y = add_property_drag_value(tree, grid, section, "Box Y", half_extents.y, 0.05, 2);
bindings.insert(
y,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeBoxY),
},
);
let z = add_property_drag_value(tree, grid, section, "Box Z", half_extents.z, 0.05, 2);
bindings.insert(
z,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::ShapeBoxZ),
},
);
}
}
let position_x =
add_property_drag_value(tree, grid, section, "Pos X", emitter.position.x, 0.05, 2);
bindings.insert(
position_x,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::PositionX),
},
);
let position_y =
add_property_drag_value(tree, grid, section, "Pos Y", emitter.position.y, 0.05, 2);
bindings.insert(
position_y,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::PositionY),
},
);
let position_z =
add_property_drag_value(tree, grid, section, "Pos Z", emitter.position.z, 0.05, 2);
bindings.insert(
position_z,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::PositionZ),
},
);
let direction_x =
add_property_drag_value(tree, grid, section, "Dir X", emitter.direction.x, 0.05, 3);
bindings.insert(
direction_x,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::DirectionX),
},
);
let direction_y =
add_property_drag_value(tree, grid, section, "Dir Y", emitter.direction.y, 0.05, 3);
bindings.insert(
direction_y,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::DirectionY),
},
);
let direction_z =
add_property_drag_value(tree, grid, section, "Dir Z", emitter.direction.z, 0.05, 3);
bindings.insert(
direction_z,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::DirectionZ),
},
);
let spawn_rate = add_property_drag_value(
tree,
grid,
section,
"Spawn rate",
emitter.spawn_rate,
1.0,
1,
);
bindings.insert(
spawn_rate,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::SpawnRate),
},
);
let burst_count = add_property_drag_value(
tree,
grid,
section,
"Burst count",
emitter.burst_count as f32,
1.0,
0,
);
bindings.insert(
burst_count,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::BurstCount),
},
);
let lifetime_min = add_property_drag_value(
tree,
grid,
section,
"Lifetime min",
emitter.particle_lifetime_min,
0.05,
2,
);
bindings.insert(
lifetime_min,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::LifetimeMin),
},
);
let lifetime_max = add_property_drag_value(
tree,
grid,
section,
"Lifetime max",
emitter.particle_lifetime_max,
0.05,
2,
);
bindings.insert(
lifetime_max,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::LifetimeMax),
},
);
let velocity_min = add_property_drag_value(
tree,
grid,
section,
"Velocity min",
emitter.initial_velocity_min,
0.05,
2,
);
bindings.insert(
velocity_min,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::VelocityMin),
},
);
let velocity_max = add_property_drag_value(
tree,
grid,
section,
"Velocity max",
emitter.initial_velocity_max,
0.05,
2,
);
bindings.insert(
velocity_max,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::VelocityMax),
},
);
let spread = add_property_drag_value(
tree,
grid,
section,
"Spread (rad)",
emitter.velocity_spread,
0.01,
3,
);
bindings.insert(
spread,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::VelocitySpread),
},
);
let gravity_x =
add_property_drag_value(tree, grid, section, "Gravity X", emitter.gravity.x, 0.05, 2);
bindings.insert(
gravity_x,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::GravityX),
},
);
let gravity_y =
add_property_drag_value(tree, grid, section, "Gravity Y", emitter.gravity.y, 0.05, 2);
bindings.insert(
gravity_y,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::GravityY),
},
);
let gravity_z =
add_property_drag_value(tree, grid, section, "Gravity Z", emitter.gravity.z, 0.05, 2);
bindings.insert(
gravity_z,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::GravityZ),
},
);
let drag_widget = add_property_drag_value(tree, grid, section, "Drag", emitter.drag, 0.01, 3);
bindings.insert(
drag_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::Drag),
},
);
let size_start = add_property_drag_value(
tree,
grid,
section,
"Size start",
emitter.size_start,
0.01,
3,
);
bindings.insert(
size_start,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::SizeStart),
},
);
let size_end =
add_property_drag_value(tree, grid, section, "Size end", emitter.size_end, 0.01, 3);
bindings.insert(
size_end,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::SizeEnd),
},
);
let emissive = add_property_drag_value(
tree,
grid,
section,
"Emissive",
emitter.emissive_strength,
0.1,
2,
);
bindings.insert(
emissive,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::EmissiveStrength),
},
);
let one_shot = add_property_checkbox(tree, grid, section, "One shot", emitter.one_shot);
bindings.insert(
one_shot,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::OneShot),
},
);
let turbulence = add_property_drag_value(
tree,
grid,
section,
"Turbulence",
emitter.turbulence_strength,
0.01,
3,
);
bindings.insert(
turbulence,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::TurbulenceStrength),
},
);
let turbulence_frequency = add_property_drag_value(
tree,
grid,
section,
"Turbulence freq",
emitter.turbulence_frequency,
0.01,
3,
);
bindings.insert(
turbulence_frequency,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::TurbulenceFrequency),
},
);
let texture_index = add_property_drag_value(
tree,
grid,
section,
"Texture index",
emitter.texture_index as f32,
1.0,
0,
);
bindings.insert(
texture_index,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::TextureIndex),
},
);
let size_curve = emitter.size_curve.clone();
let opacity_curve = emitter.opacity_curve.clone();
let death_sub = emitter.death_sub_emitter.clone().unwrap_or_default();
let trail_sub = emitter.trail_sub_emitter.clone().unwrap_or_default();
let trail_interval = emitter.trail_spawn_interval;
let size_section = tree.add_property_section(grid, "Size Curve");
for (index, key) in size_curve.iter().enumerate() {
let time_widget = add_property_drag_value(
tree,
grid,
size_section,
&format!("Time {}", index),
key.0,
0.05,
2,
);
bindings.insert(
time_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::SizeCurveTime(index)),
},
);
let value_widget = add_property_drag_value(
tree,
grid,
size_section,
&format!("Size {}", index),
key.1,
0.01,
3,
);
bindings.insert(
value_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::SizeCurveValue(index)),
},
);
let remove_widget = add_property_button(tree, grid, size_section, "Remove key", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::RemoveSizeCurveKey(index)),
},
);
}
let add_size_widget = add_property_button(tree, grid, size_section, "Size keys", "Add key");
bindings.insert(
add_size_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::AddSizeCurveKey),
},
);
let opacity_section = tree.add_property_section(grid, "Opacity Curve");
for (index, key) in opacity_curve.iter().enumerate() {
let time_widget = add_property_drag_value(
tree,
grid,
opacity_section,
&format!("Time {}", index),
key.0,
0.05,
2,
);
bindings.insert(
time_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::OpacityCurveTime(index)),
},
);
let value_widget = add_property_drag_value(
tree,
grid,
opacity_section,
&format!("Opacity {}", index),
key.1,
0.05,
2,
);
bindings.insert(
value_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::OpacityCurveValue(index)),
},
);
let remove_widget =
add_property_button(tree, grid, opacity_section, "Remove key", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::RemoveOpacityCurveKey(index)),
},
);
}
let add_opacity_widget =
add_property_button(tree, grid, opacity_section, "Opacity keys", "Add key");
bindings.insert(
add_opacity_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::AddOpacityCurveKey),
},
);
let sub_section = tree.add_property_section(grid, "Sub-emitters");
let death = add_property_text_input(
tree,
grid,
sub_section,
"On death",
&death_sub,
"prefab path",
);
bindings.insert(
death,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::DeathSubEmitter),
},
);
let trail =
add_property_text_input(tree, grid, sub_section, "Trail", &trail_sub, "prefab path");
bindings.insert(
trail,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::TrailSubEmitter),
},
);
let trail_interval_widget = add_property_drag_value(
tree,
grid,
sub_section,
"Trail interval",
trail_interval,
0.01,
3,
);
bindings.insert(
trail_interval_widget,
InspectorBinding {
entity,
field: InspectorField::Particle(ParticleField::TrailSpawnInterval),
},
);
}
fn populate_decal_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(decal) = tree.world_mut().core.get_decal(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Decal");
let texture_input = add_property_text_input(
tree,
grid,
section,
"Texture",
decal.texture.as_deref().unwrap_or(""),
"path",
);
bindings.insert(
texture_input,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::Texture),
},
);
let emissive_input = add_property_text_input(
tree,
grid,
section,
"Emissive tex",
decal.emissive_texture.as_deref().unwrap_or(""),
"path",
);
bindings.insert(
emissive_input,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::EmissiveTexture),
},
);
let color = add_property_color_picker(
tree,
grid,
section,
"Color",
vec4(
decal.color[0],
decal.color[1],
decal.color[2],
decal.color[3],
),
);
bindings.insert(
color,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::Color),
},
);
let emissive = add_property_drag_value(
tree,
grid,
section,
"Emissive",
decal.emissive_strength,
0.05,
2,
);
bindings.insert(
emissive,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::EmissiveStrength),
},
);
let size_x = add_property_drag_value(tree, grid, section, "Size X", decal.size.x, 0.05, 2);
bindings.insert(
size_x,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::SizeX),
},
);
let size_y = add_property_drag_value(tree, grid, section, "Size Y", decal.size.y, 0.05, 2);
bindings.insert(
size_y,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::SizeY),
},
);
let depth = add_property_drag_value(tree, grid, section, "Depth", decal.depth, 0.05, 2);
bindings.insert(
depth,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::Depth),
},
);
let normal_threshold = add_property_drag_value(
tree,
grid,
section,
"Normal threshold",
decal.normal_threshold,
0.01,
3,
);
bindings.insert(
normal_threshold,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::NormalThreshold),
},
);
let fade_start =
add_property_drag_value(tree, grid, section, "Fade start", decal.fade_start, 0.5, 2);
bindings.insert(
fade_start,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::FadeStart),
},
);
let fade_end = add_property_drag_value(tree, grid, section, "Fade end", decal.fade_end, 0.5, 2);
bindings.insert(
fade_end,
InspectorBinding {
entity,
field: InspectorField::Decal(DecalField::FadeEnd),
},
);
}
fn populate_audio_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(source) = tree.world_mut().core.get_audio_source(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Audio");
let audio_ref = add_property_text_input(
tree,
grid,
section,
"Reference",
source.audio_ref.as_deref().unwrap_or(""),
"audio path",
);
bindings.insert(
audio_ref,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::AudioRef),
},
);
let volume = add_property_drag_value(tree, grid, section, "Volume", source.volume, 0.01, 3);
bindings.insert(
volume,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::Volume),
},
);
let looping = add_property_checkbox(tree, grid, section, "Loop", source.looping);
bindings.insert(
looping,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::Looping),
},
);
let playing = add_property_checkbox(tree, grid, section, "Playing", source.playing);
bindings.insert(
playing,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::Playing),
},
);
let spatial = add_property_checkbox(tree, grid, section, "Spatial", source.spatial);
bindings.insert(
spatial,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::Spatial),
},
);
let bus_index = match source.bus {
nightshade::ecs::audio::components::AudioBus::Master => 0,
nightshade::ecs::audio::components::AudioBus::Music => 1,
nightshade::ecs::audio::components::AudioBus::Sfx => 2,
nightshade::ecs::audio::components::AudioBus::Ambient => 3,
nightshade::ecs::audio::components::AudioBus::Voice => 4,
nightshade::ecs::audio::components::AudioBus::Ui => 5,
};
let bus = add_property_dropdown(
tree,
grid,
section,
"Bus",
&["Master", "Music", "Sfx", "Ambient", "Voice", "Ui"],
bus_index,
);
bindings.insert(
bus,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::Bus),
},
);
let min_distance = add_property_drag_value(
tree,
grid,
section,
"Min distance",
source.min_distance,
0.1,
2,
);
bindings.insert(
min_distance,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::MinDistance),
},
);
let max_distance = add_property_drag_value(
tree,
grid,
section,
"Max distance",
source.max_distance,
0.1,
2,
);
bindings.insert(
max_distance,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::MaxDistance),
},
);
let zones = source.reverb_zones.clone();
for (index, (zone_name, zone_send)) in zones.iter().enumerate() {
let name_widget = add_property_text_input(
tree,
grid,
section,
&format!("Reverb zone {}", index),
zone_name,
"zone name",
);
bindings.insert(
name_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::ReverbZoneName(index)),
},
);
let send_widget = add_property_drag_value(
tree,
grid,
section,
&format!("Reverb send {}", index),
*zone_send,
0.1,
2,
);
bindings.insert(
send_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::ReverbZoneSend(index)),
},
);
let remove_widget = add_property_button(tree, grid, section, "Remove", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::RemoveReverbZone(index)),
},
);
}
let add_widget = add_property_button(tree, grid, section, "Reverb zones", "Add zone");
bindings.insert(
add_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::AddReverbZone),
},
);
let random_pick = source.random_pick;
let random_clips = source.random_clips.clone();
let random_section = tree.add_property_section(grid, "Random Clips");
let random_pick_widget =
add_property_checkbox(tree, grid, random_section, "Random pick", random_pick);
bindings.insert(
random_pick_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::RandomPick),
},
);
for (index, clip) in random_clips.iter().enumerate() {
let clip_widget = add_property_text_input(
tree,
grid,
random_section,
&format!("Clip {}", index),
clip,
"path/to/clip",
);
bindings.insert(
clip_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::RandomClip(index)),
},
);
let remove_widget = add_property_button(tree, grid, random_section, "Remove", "Remove");
bindings.insert(
remove_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::RemoveRandomClip(index)),
},
);
}
let add_clip_widget =
add_property_button(tree, grid, random_section, "Random clips", "Add clip");
bindings.insert(
add_clip_widget,
InspectorBinding {
entity,
field: InspectorField::Audio(AudioField::AddRandomClip),
},
);
}
fn populate_rigid_body_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(body) = tree.world_mut().core.get_rigid_body(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Rigid body");
let type_index = match body.body_type {
nightshade::ecs::physics::types::RigidBodyType::Dynamic => 0,
nightshade::ecs::physics::types::RigidBodyType::KinematicPositionBased => 1,
nightshade::ecs::physics::types::RigidBodyType::KinematicVelocityBased => 2,
nightshade::ecs::physics::types::RigidBodyType::Fixed => 3,
};
let body_type = add_property_dropdown(
tree,
grid,
section,
"Type",
&["Dynamic", "Kinematic Pos", "Kinematic Vel", "Fixed"],
type_index,
);
bindings.insert(
body_type,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::BodyType),
},
);
let mass = add_property_drag_value(tree, grid, section, "Mass", body.mass, 0.1, 3);
bindings.insert(
mass,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::Mass),
},
);
let linvel_x =
add_property_drag_value(tree, grid, section, "Linvel X", body.linvel[0], 0.05, 3);
bindings.insert(
linvel_x,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::LinvelX),
},
);
let linvel_y =
add_property_drag_value(tree, grid, section, "Linvel Y", body.linvel[1], 0.05, 3);
bindings.insert(
linvel_y,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::LinvelY),
},
);
let linvel_z =
add_property_drag_value(tree, grid, section, "Linvel Z", body.linvel[2], 0.05, 3);
bindings.insert(
linvel_z,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::LinvelZ),
},
);
let angvel_x =
add_property_drag_value(tree, grid, section, "Angvel X", body.angvel[0], 0.05, 3);
bindings.insert(
angvel_x,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::AngvelX),
},
);
let angvel_y =
add_property_drag_value(tree, grid, section, "Angvel Y", body.angvel[1], 0.05, 3);
bindings.insert(
angvel_y,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::AngvelY),
},
);
let angvel_z =
add_property_drag_value(tree, grid, section, "Angvel Z", body.angvel[2], 0.05, 3);
bindings.insert(
angvel_z,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::AngvelZ),
},
);
let ccd = add_property_checkbox(tree, grid, section, "CCD", body.ccd_enabled);
bindings.insert(
ccd,
InspectorBinding {
entity,
field: InspectorField::RigidBody(RigidBodyField::Ccd),
},
);
}
fn populate_collider_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(collider) = tree.world_mut().core.get_collider(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Collider");
let shape_index = match &collider.shape {
ColliderShape::Ball { .. } => 0,
ColliderShape::Cuboid { .. } => 1,
ColliderShape::Capsule { .. } => 2,
ColliderShape::Cylinder { .. } => 3,
ColliderShape::Cone { .. } => 4,
ColliderShape::ConvexMesh { .. } => 5,
ColliderShape::TriMesh { .. } => 6,
ColliderShape::HeightField { .. } => 7,
};
let shape = add_property_dropdown(
tree,
grid,
section,
"Shape",
&[
"Ball",
"Cuboid",
"Capsule",
"Cylinder",
"Cone",
"ConvexMesh",
"TriMesh",
"HeightField",
],
shape_index,
);
bindings.insert(
shape,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::Shape),
},
);
match &collider.shape {
ColliderShape::Ball { radius } => {
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::BallRadius),
},
);
}
ColliderShape::Cuboid { hx, hy, hz } => {
let x = add_property_drag_value(tree, grid, section, "Half X", *hx, 0.05, 3);
bindings.insert(
x,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CuboidX),
},
);
let y = add_property_drag_value(tree, grid, section, "Half Y", *hy, 0.05, 3);
bindings.insert(
y,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CuboidY),
},
);
let z = add_property_drag_value(tree, grid, section, "Half Z", *hz, 0.05, 3);
bindings.insert(
z,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CuboidZ),
},
);
}
ColliderShape::Capsule {
half_height,
radius,
} => {
let hh =
add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
bindings.insert(
hh,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CapsuleHalfHeight),
},
);
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CapsuleRadius),
},
);
}
ColliderShape::Cylinder {
half_height,
radius,
} => {
let hh =
add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
bindings.insert(
hh,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CylinderHalfHeight),
},
);
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CylinderRadius),
},
);
}
ColliderShape::Cone {
half_height,
radius,
} => {
let hh =
add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
bindings.insert(
hh,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::ConeHalfHeight),
},
);
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::ConeRadius),
},
);
}
ColliderShape::ConvexMesh { vertices } => {
add_property_value(tree, grid, section, "Vertices", &vertices.len().to_string());
}
ColliderShape::TriMesh { vertices, indices } => {
add_property_value(tree, grid, section, "Vertices", &vertices.len().to_string());
add_property_value(tree, grid, section, "Triangles", &indices.len().to_string());
}
ColliderShape::HeightField {
nrows,
ncols,
heights,
scale,
} => {
let _ = heights;
let _ = scale;
add_property_value(
tree,
grid,
section,
"Resolution",
&format!("{nrows}x{ncols}"),
);
}
}
let friction =
add_property_drag_value(tree, grid, section, "Friction", collider.friction, 0.01, 3);
bindings.insert(
friction,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::Friction),
},
);
let restitution = add_property_drag_value(
tree,
grid,
section,
"Restitution",
collider.restitution,
0.01,
3,
);
bindings.insert(
restitution,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::Restitution),
},
);
let density =
add_property_drag_value(tree, grid, section, "Density", collider.density, 0.05, 3);
bindings.insert(
density,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::Density),
},
);
let is_sensor = add_property_checkbox(tree, grid, section, "Sensor", collider.is_sensor);
bindings.insert(
is_sensor,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::IsSensor),
},
);
let collision_membership_widget = add_property_drag_value(
tree,
grid,
section,
"Collision Memberships",
collider.collision_groups.memberships as f32,
1.0,
0,
);
bindings.insert(
collision_membership_widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CollisionMemberships),
},
);
let collision_filter_widget = add_property_drag_value(
tree,
grid,
section,
"Collision Filter",
collider.collision_groups.filter as f32,
1.0,
0,
);
bindings.insert(
collision_filter_widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::CollisionFilter),
},
);
let solver_membership_widget = add_property_drag_value(
tree,
grid,
section,
"Solver Memberships",
collider.solver_groups.memberships as f32,
1.0,
0,
);
bindings.insert(
solver_membership_widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::SolverMemberships),
},
);
let solver_filter_widget = add_property_drag_value(
tree,
grid,
section,
"Solver Filter",
collider.solver_groups.filter as f32,
1.0,
0,
);
bindings.insert(
solver_filter_widget,
InspectorBinding {
entity,
field: InspectorField::Collider(ColliderField::SolverFilter),
},
);
}
fn populate_character_controller_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(controller) = tree
.world_mut()
.core
.get_character_controller(entity)
.cloned()
else {
return;
};
let section = tree.add_property_section(grid, "Character controller");
let shape_index = match controller.shape {
ColliderShape::Capsule { .. } => 0,
ColliderShape::Ball { .. } => 1,
ColliderShape::Cuboid { .. } => 2,
_ => 0,
};
let shape = add_property_dropdown(
tree,
grid,
section,
"Shape",
&["Capsule", "Ball", "Cuboid"],
shape_index,
);
bindings.insert(
shape,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::Shape),
},
);
match &controller.shape {
ColliderShape::Capsule {
half_height,
radius,
} => {
let hh =
add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
bindings.insert(
hh,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::CapsuleHalfHeight,
),
},
);
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::CapsuleRadius,
),
},
);
}
ColliderShape::Ball { radius } => {
let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::BallRadius,
),
},
);
}
ColliderShape::Cuboid { hx, hy, hz } => {
let x = add_property_drag_value(tree, grid, section, "Half X", *hx, 0.05, 3);
bindings.insert(
x,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::CuboidX),
},
);
let y = add_property_drag_value(tree, grid, section, "Half Y", *hy, 0.05, 3);
bindings.insert(
y,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::CuboidY),
},
);
let z = add_property_drag_value(tree, grid, section, "Half Z", *hz, 0.05, 3);
bindings.insert(
z,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::CuboidZ),
},
);
}
_ => {}
}
let max_speed = add_property_drag_value(
tree,
grid,
section,
"Max speed",
controller.max_speed,
0.05,
2,
);
bindings.insert(
max_speed,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::MaxSpeed),
},
);
let acceleration = add_property_drag_value(
tree,
grid,
section,
"Acceleration",
controller.acceleration,
0.5,
2,
);
bindings.insert(
acceleration,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::Acceleration),
},
);
let jump = add_property_drag_value(
tree,
grid,
section,
"Jump impulse",
controller.jump_impulse,
0.05,
2,
);
bindings.insert(
jump,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::JumpImpulse),
},
);
let scale = add_property_drag_value(tree, grid, section, "Scale", controller.scale, 0.05, 2);
bindings.insert(
scale,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::Scale),
},
);
let crouch_enabled = add_property_checkbox(
tree,
grid,
section,
"Crouch enabled",
controller.crouch_enabled,
);
bindings.insert(
crouch_enabled,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::CrouchEnabled),
},
);
let crouch_mult = add_property_drag_value(
tree,
grid,
section,
"Crouch speed mult",
controller.crouch_speed_multiplier,
0.01,
2,
);
bindings.insert(
crouch_mult,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::CrouchSpeedMultiplier,
),
},
);
let sprint_mult = add_property_drag_value(
tree,
grid,
section,
"Sprint speed mult",
controller.sprint_speed_multiplier,
0.01,
2,
);
bindings.insert(
sprint_mult,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::SprintSpeedMultiplier,
),
},
);
let standing = add_property_drag_value(
tree,
grid,
section,
"Standing half h",
controller.standing_half_height,
0.01,
3,
);
bindings.insert(
standing,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::StandingHalfHeight,
),
},
);
let crouching = add_property_drag_value(
tree,
grid,
section,
"Crouching half h",
controller.crouching_half_height,
0.01,
3,
);
bindings.insert(
crouching,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::CrouchingHalfHeight,
),
},
);
let offset = add_property_drag_value(
tree,
grid,
section,
"Config offset",
controller.config.offset,
0.001,
4,
);
bindings.insert(
offset,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::ConfigOffset),
},
);
let max_slope = add_property_drag_value(
tree,
grid,
section,
"Max slope climb (\u{00b0})",
controller.config.max_slope_climb_angle.to_degrees(),
0.5,
1,
);
bindings.insert(
max_slope,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::MaxSlopeClimb),
},
);
let min_slope = add_property_drag_value(
tree,
grid,
section,
"Min slope slide (\u{00b0})",
controller.config.min_slope_slide_angle.to_degrees(),
0.5,
1,
);
bindings.insert(
min_slope,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::MinSlopeSlide),
},
);
let friction = add_property_drag_value(
tree,
grid,
section,
"Friction rate",
controller.friction_rate,
0.05,
2,
);
bindings.insert(
friction,
InspectorBinding {
entity,
field: InspectorField::CharacterController(CharacterControllerField::FrictionRate),
},
);
let above_max = add_property_drag_value(
tree,
grid,
section,
"Above-max friction",
controller.above_max_friction_rate,
0.05,
2,
);
bindings.insert(
above_max,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::AboveMaxFrictionRate,
),
},
);
let engine_input = add_property_checkbox(
tree,
grid,
section,
"Engine input",
controller.engine_input_enabled,
);
bindings.insert(
engine_input,
InspectorBinding {
entity,
field: InspectorField::CharacterController(
CharacterControllerField::EngineInputEnabled,
),
},
);
add_property_value(
tree,
grid,
section,
"Grounded",
if controller.grounded { "yes" } else { "no" },
);
}
fn populate_navmesh_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(agent) = tree.world_mut().core.get_navmesh_agent(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Navmesh agent");
let speed =
add_property_drag_value(tree, grid, section, "Speed", agent.movement_speed, 0.05, 3);
bindings.insert(
speed,
InspectorBinding {
entity,
field: InspectorField::Navmesh(NavmeshField::MovementSpeed),
},
);
let arrival = add_property_drag_value(
tree,
grid,
section,
"Arrival threshold",
agent.arrival_threshold,
0.01,
3,
);
bindings.insert(
arrival,
InspectorBinding {
entity,
field: InspectorField::Navmesh(NavmeshField::ArrivalThreshold),
},
);
let recalc = add_property_drag_value(
tree,
grid,
section,
"Recalc threshold",
agent.path_recalculation_threshold,
0.05,
3,
);
bindings.insert(
recalc,
InspectorBinding {
entity,
field: InspectorField::Navmesh(NavmeshField::PathRecalculationThreshold),
},
);
let radius =
add_property_drag_value(tree, grid, section, "Radius", agent.agent_radius, 0.05, 3);
bindings.insert(
radius,
InspectorBinding {
entity,
field: InspectorField::Navmesh(NavmeshField::AgentRadius),
},
);
let height =
add_property_drag_value(tree, grid, section, "Height", agent.agent_height, 0.05, 3);
bindings.insert(
height,
InspectorBinding {
entity,
field: InspectorField::Navmesh(NavmeshField::AgentHeight),
},
);
}
fn populate_text_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(text) = tree.world_mut().core.get_text(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Text");
let content_string = tree
.world_mut()
.resources
.text
.cache
.get_text(text.text_index)
.unwrap_or("")
.to_string();
let content = add_property_text_input(tree, grid, section, "Content", &content_string, "Text");
bindings.insert(
content,
InspectorBinding {
entity,
field: InspectorField::Text(TextField::Content),
},
);
let font_size = add_property_drag_value(
tree,
grid,
section,
"Font size",
text.properties.font_size,
0.5,
1,
);
bindings.insert(
font_size,
InspectorBinding {
entity,
field: InspectorField::Text(TextField::FontSize),
},
);
let color = add_property_color_picker(
tree,
grid,
section,
"Color",
vec4(
text.properties.color.x,
text.properties.color.y,
text.properties.color.z,
text.properties.color.w,
),
);
bindings.insert(
color,
InspectorBinding {
entity,
field: InspectorField::Text(TextField::Color),
},
);
}
fn populate_grass_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(region) = tree.world_mut().core.get_grass_region(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Grass region");
let enabled = add_property_checkbox(tree, grid, section, "Enabled", region.enabled);
bindings.insert(
enabled,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::Enabled),
},
);
let blades = add_property_drag_value(
tree,
grid,
section,
"Blades / patch",
region.config.blades_per_patch as f32,
1.0,
0,
);
bindings.insert(
blades,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::BladesPerPatch),
},
);
let patch_size = add_property_drag_value(
tree,
grid,
section,
"Patch size",
region.config.patch_size,
0.1,
2,
);
bindings.insert(
patch_size,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::PatchSize),
},
);
let stream_radius = add_property_drag_value(
tree,
grid,
section,
"Stream radius",
region.config.stream_radius,
1.0,
1,
);
bindings.insert(
stream_radius,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::StreamRadius),
},
);
let wind_strength = add_property_drag_value(
tree,
grid,
section,
"Wind strength",
region.config.wind_strength,
0.05,
2,
);
bindings.insert(
wind_strength,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::WindStrength),
},
);
let wind_freq = add_property_drag_value(
tree,
grid,
section,
"Wind freq",
region.config.wind_frequency,
0.05,
2,
);
bindings.insert(
wind_freq,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::WindFrequency),
},
);
let wind_x = add_property_drag_value(
tree,
grid,
section,
"Wind dir X",
region.config.wind_direction[0],
0.05,
3,
);
bindings.insert(
wind_x,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::WindDirX),
},
);
let wind_z = add_property_drag_value(
tree,
grid,
section,
"Wind dir Z",
region.config.wind_direction[1],
0.05,
3,
);
bindings.insert(
wind_z,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::WindDirZ),
},
);
let interaction_radius = add_property_drag_value(
tree,
grid,
section,
"Interaction r",
region.config.interaction_radius,
0.05,
2,
);
bindings.insert(
interaction_radius,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::InteractionRadius),
},
);
let interaction_strength = add_property_drag_value(
tree,
grid,
section,
"Interaction s",
region.config.interaction_strength,
0.05,
2,
);
bindings.insert(
interaction_strength,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::InteractionStrength),
},
);
let cast_shadows = add_property_checkbox(
tree,
grid,
section,
"Cast shadows",
region.config.cast_shadows,
);
bindings.insert(
cast_shadows,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::CastShadows),
},
);
let receive_shadows = add_property_checkbox(
tree,
grid,
section,
"Receive shadows",
region.config.receive_shadows,
);
bindings.insert(
receive_shadows,
InspectorBinding {
entity,
field: InspectorField::Grass(GrassField::ReceiveShadows),
},
);
}
fn populate_grass_interactor_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(interactor) = tree.world_mut().core.get_grass_interactor(entity).cloned() else {
return;
};
let section = tree.add_property_section(grid, "Grass interactor");
let radius = add_property_drag_value(tree, grid, section, "Radius", interactor.radius, 0.05, 3);
bindings.insert(
radius,
InspectorBinding {
entity,
field: InspectorField::GrassInteractor(GrassInteractorField::Radius),
},
);
let strength = add_property_drag_value(
tree,
grid,
section,
"Strength",
interactor.strength,
0.05,
3,
);
bindings.insert(
strength,
InspectorBinding {
entity,
field: InspectorField::GrassInteractor(GrassInteractorField::Strength),
},
);
}
fn populate_misc_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
scene_snapshot: &SceneStructureSnapshot,
) {
let scene_section = tree.add_property_section(grid, "Scene Layer & Chunk");
let mut layer_options: Vec<String> = vec!["(none)".to_string()];
let mut layer_index = 0usize;
for (index, layer) in scene_snapshot.layers.iter().enumerate() {
layer_options.push(layer.name.clone());
if scene_snapshot.entity_layer == Some(layer.id) {
layer_index = index + 1;
}
}
let layer_refs: Vec<&str> = layer_options.iter().map(|s| s.as_str()).collect();
let layer_widget =
add_property_dropdown(tree, grid, scene_section, "Layer", &layer_refs, layer_index);
bindings.insert(
layer_widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::SceneLayerAssignment),
},
);
let mut chunk_options: Vec<String> = vec!["(none)".to_string()];
let mut chunk_index = 0usize;
for (index, chunk) in scene_snapshot.chunks.iter().enumerate() {
chunk_options.push(chunk.name.clone());
if scene_snapshot.entity_chunk == Some(chunk.id) {
chunk_index = index + 1;
}
}
let chunk_refs: Vec<&str> = chunk_options.iter().map(|s| s.as_str()).collect();
let chunk_widget =
add_property_dropdown(tree, grid, scene_section, "Chunk", &chunk_refs, chunk_index);
bindings.insert(
chunk_widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::SceneChunkAssignment),
},
);
let has_layer = tree.world_mut().core.entity_has_render_layer(entity);
let has_mask = tree.world_mut().core.entity_has_culling_mask(entity);
let has_cam_mask = tree.world_mut().core.entity_has_camera_culling_mask(entity);
let has_ignore = tree.world_mut().core.entity_has_ignore_parent_scale(entity);
if !has_layer && !has_mask && !has_cam_mask && !has_ignore {
return;
}
let section = tree.add_property_section(grid, "Layers & masks");
if let Some(layer) = tree.world_mut().core.get_render_layer(entity).copied() {
let widget = add_property_dropdown(
tree,
grid,
section,
"Render layer",
&["World", "Overlay"],
if layer.0 == nightshade::ecs::primitives::RenderLayer::OVERLAY {
1
} else {
0
},
);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::RenderLayer),
},
);
}
if let Some(mask) = tree.world_mut().core.get_culling_mask(entity).copied() {
for bit in 0u8..8 {
let set = mask.0 & (1u32 << bit) != 0;
let widget =
add_property_checkbox(tree, grid, section, &format!("Layer bit {bit}"), set);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::CullingMaskBit(bit)),
},
);
}
}
if let Some(mask) = tree
.world_mut()
.core
.get_camera_culling_mask(entity)
.copied()
{
for bit in 0u8..8 {
let set = mask.0 & (1u32 << bit) != 0;
let widget =
add_property_checkbox(tree, grid, section, &format!("Camera bit {bit}"), set);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::CameraCullingMaskBit(bit)),
},
);
}
}
if has_ignore {
let widget = add_property_checkbox(tree, grid, section, "Ignore parent scale", true);
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::IgnoreParentScale),
},
);
}
if let Some(lines) = tree.world_mut().core.get_lines(entity) {
let line_count = lines.lines.len();
let always_on_top = lines.always_on_top;
let lines_section = tree.add_property_section(grid, "Lines");
add_property_value(
tree,
grid,
lines_section,
"Segments",
&line_count.to_string(),
);
let always_on_top_widget =
add_property_checkbox(tree, grid, lines_section, "Always on top", always_on_top);
bindings.insert(
always_on_top_widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::LinesAlwaysOnTop),
},
);
let clear_widget = add_property_button(tree, grid, lines_section, "Clear lines", "Clear");
bindings.insert(
clear_widget,
InspectorBinding {
entity,
field: InspectorField::Misc(MiscField::LinesClear),
},
);
}
}
fn populate_add_component_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let section = tree.add_property_section(grid, "Add component");
for (kind, label, exists) in available_components(tree.world_mut(), entity) {
if exists {
continue;
}
let widget = add_property_button(tree, grid, section, label, "Add");
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::AddComponent(kind),
},
);
}
}
fn populate_remove_component_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let present: Vec<(ComponentKind, &'static str)> =
available_components(tree.world_mut(), entity)
.into_iter()
.filter_map(|(kind, label, exists)| if exists { Some((kind, label)) } else { None })
.collect();
if present.is_empty() {
return;
}
let section = tree.add_property_section(grid, "Remove component");
for (kind, label) in present {
let widget = add_property_button(tree, grid, section, label, "Remove");
bindings.insert(
widget,
InspectorBinding {
entity,
field: InspectorField::RemoveComponent(kind),
},
);
}
}
fn available_components(
world: &mut World,
entity: Entity,
) -> Vec<(ComponentKind, &'static str, bool)> {
vec![
(
ComponentKind::ParticleEmitter,
"Particle emitter",
world.core.entity_has_particle_emitter(entity),
),
(
ComponentKind::Decal,
"Decal",
world.core.entity_has_decal(entity),
),
(
ComponentKind::AudioSource,
"Audio source",
world.core.entity_has_audio_source(entity),
),
(
ComponentKind::RigidBody,
"Rigid body",
world.core.entity_has_rigid_body(entity),
),
(
ComponentKind::Collider,
"Collider",
world.core.entity_has_collider(entity),
),
(
ComponentKind::CharacterController,
"Character controller",
world.core.entity_has_character_controller(entity),
),
(
ComponentKind::NavmeshAgent,
"Navmesh agent",
world.core.entity_has_navmesh_agent(entity),
),
(
ComponentKind::Camera,
"Camera",
world.core.entity_has_camera(entity),
),
(
ComponentKind::GrassRegion,
"Grass region",
world.core.entity_has_grass_region(entity),
),
(
ComponentKind::GrassInteractor,
"Grass interactor",
world.core.entity_has_grass_interactor(entity),
),
(
ComponentKind::Text,
"Text",
world.core.entity_has_text(entity),
),
(
ComponentKind::AnimationPlayer,
"Animation player",
world.core.entity_has_animation_player(entity),
),
(
ComponentKind::Light,
"Light",
world.core.entity_has_light(entity),
),
(
ComponentKind::Visibility,
"Visibility",
world.core.entity_has_visibility(entity),
),
(
ComponentKind::CastsShadow,
"Casts shadow",
world.core.entity_has_casts_shadow(entity),
),
(
ComponentKind::RenderLayer,
"Render layer",
world.core.entity_has_render_layer(entity),
),
(
ComponentKind::CullingMask,
"Culling mask",
world.core.entity_has_culling_mask(entity),
),
(
ComponentKind::CameraCullingMask,
"Camera culling mask",
world.core.entity_has_camera_culling_mask(entity),
),
(
ComponentKind::IgnoreParentScale,
"Ignore parent scale",
world.core.entity_has_ignore_parent_scale(entity),
),
(
ComponentKind::AudioListener,
"Audio listener",
world.core.entity_has_audio_listener(entity),
),
(
ComponentKind::CollisionListener,
"Collision listener",
world.core.entity_has_collision_listener(entity),
),
(
ComponentKind::PhysicsInterpolation,
"Physics interpolation",
world.core.entity_has_physics_interpolation(entity),
),
(
ComponentKind::MorphWeights,
"Morph weights",
world.core.entity_has_morph_weights(entity),
),
(
ComponentKind::MaterialVariants,
"Material variants",
world.core.entity_has_material_variants(entity),
),
(
ComponentKind::PanOrbitCamera,
"Pan-orbit camera",
world.core.entity_has_pan_orbit_camera(entity),
),
(
ComponentKind::ThirdPersonCamera,
"Third-person camera",
world.core.entity_has_third_person_camera(entity),
),
(
ComponentKind::CameraEnvironment,
"Camera environment",
world.core.entity_has_camera_environment(entity),
),
(
ComponentKind::CameraPostProcess,
"Camera post-process",
world.core.entity_has_camera_post_process(entity),
),
(
ComponentKind::ConstrainedAspect,
"Constrained aspect",
world.core.entity_has_constrained_aspect(entity),
),
(
ComponentKind::ViewportUpdateMode,
"Viewport update mode",
world.core.entity_has_viewport_update_mode(entity),
),
]
}
fn attach_default_component(world: &mut World, entity: Entity, kind: ComponentKind) {
use nightshade::ecs::world::{
ANIMATION_PLAYER, AUDIO_SOURCE, CAMERA, CAMERA_CULLING_MASK, CASTS_SHADOW, COLLIDER,
CULLING_MASK, DECAL, GRASS_INTERACTOR, GRASS_REGION, LIGHT, NAVMESH_AGENT,
PARTICLE_EMITTER, RENDER_LAYER, RIGID_BODY, TEXT, VISIBILITY,
};
match kind {
ComponentKind::ParticleEmitter => {
world.core.add_components(entity, PARTICLE_EMITTER);
world.core.set_particle_emitter(
entity,
nightshade::ecs::particles::components::ParticleEmitter::default(),
);
}
ComponentKind::Decal => {
world.core.add_components(entity, DECAL);
world
.core
.set_decal(entity, nightshade::ecs::decal::components::Decal::default());
}
ComponentKind::AudioSource => {
world.core.add_components(entity, AUDIO_SOURCE);
world.core.set_audio_source(
entity,
nightshade::ecs::audio::components::AudioSource::default(),
);
}
ComponentKind::RigidBody => {
world.core.add_components(entity, RIGID_BODY);
world
.core
.set_rigid_body(entity, RigidBodyComponent::new_dynamic());
}
ComponentKind::Collider => {
world.core.add_components(entity, COLLIDER);
world
.core
.set_collider(entity, ColliderComponent::new_cuboid(0.5, 0.5, 0.5));
}
ComponentKind::CharacterController => {
world
.core
.add_components(entity, nightshade::ecs::world::CHARACTER_CONTROLLER);
world.core.set_character_controller(
entity,
CharacterControllerComponent::new_capsule(0.9, 0.3),
);
}
ComponentKind::NavmeshAgent => {
world.core.add_components(entity, NAVMESH_AGENT);
world.core.set_navmesh_agent(entity, NavMeshAgent::new());
}
ComponentKind::Camera => {
world.core.add_components(entity, CAMERA);
world.core.set_camera(
entity,
nightshade::ecs::camera::components::Camera::default(),
);
}
ComponentKind::GrassRegion => {
world.core.add_components(entity, GRASS_REGION);
world.core.set_grass_region(
entity,
nightshade::ecs::grass::components::GrassRegion::default(),
);
}
ComponentKind::GrassInteractor => {
world.core.add_components(entity, GRASS_INTERACTOR);
world.core.set_grass_interactor(
entity,
nightshade::ecs::grass::components::GrassInteractor::new(1.0, 1.0),
);
}
ComponentKind::Text => {
let text_index = world.resources.text.cache.add_text("Text");
world.core.add_components(entity, TEXT);
world.core.set_text(
entity,
nightshade::ecs::text::components::Text::new(text_index),
);
}
ComponentKind::AnimationPlayer => {
world.core.add_components(entity, ANIMATION_PLAYER);
world.core.set_animation_player(
entity,
nightshade::ecs::animation::components::AnimationPlayer::default(),
);
}
ComponentKind::Light => {
world.core.add_components(entity, LIGHT);
world.core.set_light(entity, Light::default());
}
ComponentKind::Visibility => {
world.core.add_components(entity, VISIBILITY);
world
.core
.set_visibility(entity, Visibility { visible: true });
}
ComponentKind::CastsShadow => {
world.core.add_components(entity, CASTS_SHADOW);
world.core.set_casts_shadow(entity, CastsShadow);
}
ComponentKind::RenderLayer => {
world.core.add_components(entity, RENDER_LAYER);
world
.core
.set_render_layer(entity, nightshade::ecs::primitives::RenderLayer::default());
}
ComponentKind::CullingMask => {
world.core.add_components(entity, CULLING_MASK);
world
.core
.set_culling_mask(entity, nightshade::ecs::primitives::CullingMask::default());
}
ComponentKind::CameraCullingMask => {
world.core.add_components(entity, CAMERA_CULLING_MASK);
world.core.set_camera_culling_mask(
entity,
nightshade::ecs::primitives::CameraCullingMask::default(),
);
}
ComponentKind::IgnoreParentScale => {
world
.core
.add_components(entity, nightshade::ecs::world::IGNORE_PARENT_SCALE);
world.core.set_ignore_parent_scale(
entity,
nightshade::ecs::transform::components::IgnoreParentScale,
);
}
ComponentKind::AudioListener => {
world
.core
.add_components(entity, nightshade::ecs::world::AUDIO_LISTENER);
world
.core
.set_audio_listener(entity, nightshade::ecs::audio::components::AudioListener);
}
ComponentKind::CollisionListener => {
world
.core
.add_components(entity, nightshade::ecs::world::COLLISION_LISTENER);
world.core.set_collision_listener(
entity,
nightshade::ecs::physics::components::CollisionListener,
);
}
ComponentKind::PhysicsInterpolation => {
world
.core
.add_components(entity, nightshade::ecs::world::PHYSICS_INTERPOLATION);
world.core.set_physics_interpolation(
entity,
nightshade::ecs::physics::components::PhysicsInterpolation::default(),
);
}
ComponentKind::MorphWeights => {
world
.core
.add_components(entity, nightshade::ecs::world::MORPH_WEIGHTS);
world.core.set_morph_weights(
entity,
nightshade::ecs::morph::components::MorphWeights::default(),
);
}
ComponentKind::MaterialVariants => {
world
.core
.add_components(entity, nightshade::ecs::world::MATERIAL_VARIANTS);
world.core.set_material_variants(
entity,
nightshade::ecs::material::components::MaterialVariants::default(),
);
}
ComponentKind::PanOrbitCamera => {
world
.core
.add_components(entity, nightshade::ecs::world::PAN_ORBIT_CAMERA);
world.core.set_pan_orbit_camera(
entity,
nightshade::ecs::camera::components::PanOrbitCamera::default(),
);
}
ComponentKind::ThirdPersonCamera => {
world
.core
.add_components(entity, nightshade::ecs::world::THIRD_PERSON_CAMERA);
world.core.set_third_person_camera(
entity,
nightshade::ecs::camera::components::ThirdPersonCamera::default(),
);
}
ComponentKind::CameraEnvironment => {
world
.core
.add_components(entity, nightshade::ecs::world::CAMERA_ENVIRONMENT);
world.core.set_camera_environment(
entity,
nightshade::ecs::camera::components::CameraEnvironment::default(),
);
}
ComponentKind::CameraPostProcess => {
world
.core
.add_components(entity, nightshade::ecs::world::CAMERA_POST_PROCESS);
world.core.set_camera_post_process(
entity,
nightshade::ecs::camera::components::CameraPostProcess::default(),
);
}
ComponentKind::ConstrainedAspect => {
world
.core
.add_components(entity, nightshade::ecs::world::CONSTRAINED_ASPECT);
world.core.set_constrained_aspect(
entity,
nightshade::ecs::camera::components::ConstrainedAspect::default(),
);
}
ComponentKind::ViewportUpdateMode => {
world
.core
.add_components(entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
world.core.set_viewport_update_mode(
entity,
nightshade::ecs::camera::components::ViewportUpdateMode::default(),
);
}
}
}
fn remove_component(world: &mut World, entity: Entity, kind: ComponentKind) {
use nightshade::ecs::world::{
ANIMATION_PLAYER, AUDIO_SOURCE, CAMERA, CAMERA_CULLING_MASK, CASTS_SHADOW,
CHARACTER_CONTROLLER, COLLIDER, CULLING_MASK, DECAL, GRASS_INTERACTOR, GRASS_REGION,
IGNORE_PARENT_SCALE, LIGHT, NAVMESH_AGENT, PARTICLE_EMITTER, RENDER_LAYER, RIGID_BODY,
TEXT, VISIBILITY,
};
let mask = match kind {
ComponentKind::ParticleEmitter => PARTICLE_EMITTER,
ComponentKind::Decal => DECAL,
ComponentKind::AudioSource => AUDIO_SOURCE,
ComponentKind::RigidBody => RIGID_BODY,
ComponentKind::Collider => COLLIDER,
ComponentKind::CharacterController => CHARACTER_CONTROLLER,
ComponentKind::NavmeshAgent => NAVMESH_AGENT,
ComponentKind::Camera => CAMERA,
ComponentKind::GrassRegion => GRASS_REGION,
ComponentKind::GrassInteractor => GRASS_INTERACTOR,
ComponentKind::Text => TEXT,
ComponentKind::AnimationPlayer => ANIMATION_PLAYER,
ComponentKind::Visibility => VISIBILITY,
ComponentKind::CastsShadow => CASTS_SHADOW,
ComponentKind::Light => LIGHT,
ComponentKind::RenderLayer => RENDER_LAYER,
ComponentKind::CullingMask => CULLING_MASK,
ComponentKind::CameraCullingMask => CAMERA_CULLING_MASK,
ComponentKind::IgnoreParentScale => IGNORE_PARENT_SCALE,
ComponentKind::AudioListener => nightshade::ecs::world::AUDIO_LISTENER,
ComponentKind::CollisionListener => nightshade::ecs::world::COLLISION_LISTENER,
ComponentKind::PhysicsInterpolation => nightshade::ecs::world::PHYSICS_INTERPOLATION,
ComponentKind::MorphWeights => nightshade::ecs::world::MORPH_WEIGHTS,
ComponentKind::MaterialVariants => nightshade::ecs::world::MATERIAL_VARIANTS,
ComponentKind::PanOrbitCamera => nightshade::ecs::world::PAN_ORBIT_CAMERA,
ComponentKind::ThirdPersonCamera => nightshade::ecs::world::THIRD_PERSON_CAMERA,
ComponentKind::CameraEnvironment => nightshade::ecs::world::CAMERA_ENVIRONMENT,
ComponentKind::CameraPostProcess => nightshade::ecs::world::CAMERA_POST_PROCESS,
ComponentKind::ConstrainedAspect => nightshade::ecs::world::CONSTRAINED_ASPECT,
ComponentKind::ViewportUpdateMode => nightshade::ecs::world::VIEWPORT_UPDATE_MODE,
};
world.core.remove_components(entity, mask);
}
fn apply_character_controller_change(
world: &mut World,
entity: Entity,
field: CharacterControllerField,
value: &FieldValue,
) {
let Some(controller) = world.core.get_character_controller_mut(entity) else {
return;
};
match (field, value) {
(CharacterControllerField::Shape, FieldValue::Index(index)) => {
controller.shape = match index {
1 => ColliderShape::Ball { radius: 0.5 },
2 => ColliderShape::Cuboid {
hx: 0.3,
hy: 0.9,
hz: 0.3,
},
_ => ColliderShape::Capsule {
half_height: 0.9,
radius: 0.3,
},
};
}
(CharacterControllerField::CapsuleHalfHeight, FieldValue::Float(v)) => {
if let ColliderShape::Capsule { half_height, .. } = &mut controller.shape {
*half_height = *v;
}
}
(CharacterControllerField::CapsuleRadius, FieldValue::Float(v)) => {
if let ColliderShape::Capsule { radius, .. } = &mut controller.shape {
*radius = *v;
}
}
(CharacterControllerField::BallRadius, FieldValue::Float(v)) => {
if let ColliderShape::Ball { radius } = &mut controller.shape {
*radius = *v;
}
}
(CharacterControllerField::CuboidX, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hx, .. } = &mut controller.shape {
*hx = *v;
}
}
(CharacterControllerField::CuboidY, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hy, .. } = &mut controller.shape {
*hy = *v;
}
}
(CharacterControllerField::CuboidZ, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hz, .. } = &mut controller.shape {
*hz = *v;
}
}
(CharacterControllerField::MaxSpeed, FieldValue::Float(v)) => controller.max_speed = *v,
(CharacterControllerField::Acceleration, FieldValue::Float(v)) => {
controller.acceleration = *v
}
(CharacterControllerField::JumpImpulse, FieldValue::Float(v)) => {
controller.jump_impulse = *v
}
(CharacterControllerField::Scale, FieldValue::Float(v)) => controller.scale = *v,
(CharacterControllerField::CrouchEnabled, FieldValue::Bool(v)) => {
controller.crouch_enabled = *v
}
(CharacterControllerField::CrouchSpeedMultiplier, FieldValue::Float(v)) => {
controller.crouch_speed_multiplier = *v
}
(CharacterControllerField::SprintSpeedMultiplier, FieldValue::Float(v)) => {
controller.sprint_speed_multiplier = *v
}
(CharacterControllerField::StandingHalfHeight, FieldValue::Float(v)) => {
controller.standing_half_height = *v
}
(CharacterControllerField::CrouchingHalfHeight, FieldValue::Float(v)) => {
controller.crouching_half_height = *v
}
(CharacterControllerField::ConfigOffset, FieldValue::Float(v)) => {
controller.config.offset = *v
}
(CharacterControllerField::MaxSlopeClimb, FieldValue::Float(degrees)) => {
controller.config.max_slope_climb_angle = degrees.to_radians();
}
(CharacterControllerField::MinSlopeSlide, FieldValue::Float(degrees)) => {
controller.config.min_slope_slide_angle = degrees.to_radians();
}
(CharacterControllerField::FrictionRate, FieldValue::Float(v)) => {
controller.friction_rate = *v
}
(CharacterControllerField::AboveMaxFrictionRate, FieldValue::Float(v)) => {
controller.above_max_friction_rate = *v
}
(CharacterControllerField::EngineInputEnabled, FieldValue::Bool(v)) => {
controller.engine_input_enabled = *v
}
_ => {}
}
}
fn apply_particle_change(
world: &mut World,
entity: Entity,
field: ParticleField,
value: &FieldValue,
) {
use nightshade::ecs::particles::components::{EmitterShape, EmitterType};
let Some(emitter) = world.core.get_particle_emitter_mut(entity) else {
return;
};
match (field, value) {
(ParticleField::Enabled, FieldValue::Bool(v)) => emitter.enabled = *v,
(ParticleField::OneShot, FieldValue::Bool(v)) => emitter.one_shot = *v,
(ParticleField::EmitterType, FieldValue::Index(index)) => {
emitter.emitter_type = match index {
0 => EmitterType::Firework,
1 => EmitterType::Fire,
2 => EmitterType::Smoke,
3 => EmitterType::Sparks,
_ => EmitterType::Trail,
};
}
(ParticleField::Shape, FieldValue::Index(index)) => {
emitter.shape = match index {
0 => EmitterShape::Point,
1 => EmitterShape::Sphere { radius: 1.0 },
2 => EmitterShape::Cone {
angle: 0.3,
height: 0.1,
},
_ => EmitterShape::Box {
half_extents: Vec3::new(0.5, 0.5, 0.5),
},
};
}
(ParticleField::ShapeRadius, FieldValue::Float(v)) => {
if let EmitterShape::Sphere { radius } = &mut emitter.shape {
*radius = *v;
}
}
(ParticleField::ShapeAngle, FieldValue::Float(v)) => {
if let EmitterShape::Cone { angle, .. } = &mut emitter.shape {
*angle = *v;
}
}
(ParticleField::ShapeHeight, FieldValue::Float(v)) => {
if let EmitterShape::Cone { height, .. } = &mut emitter.shape {
*height = *v;
}
}
(ParticleField::ShapeBoxX, FieldValue::Float(v)) => {
if let EmitterShape::Box { half_extents } = &mut emitter.shape {
half_extents.x = *v;
}
}
(ParticleField::ShapeBoxY, FieldValue::Float(v)) => {
if let EmitterShape::Box { half_extents } = &mut emitter.shape {
half_extents.y = *v;
}
}
(ParticleField::ShapeBoxZ, FieldValue::Float(v)) => {
if let EmitterShape::Box { half_extents } = &mut emitter.shape {
half_extents.z = *v;
}
}
(ParticleField::PositionX, FieldValue::Float(v)) => emitter.position.x = *v,
(ParticleField::PositionY, FieldValue::Float(v)) => emitter.position.y = *v,
(ParticleField::PositionZ, FieldValue::Float(v)) => emitter.position.z = *v,
(ParticleField::DirectionX, FieldValue::Float(v)) => emitter.direction.x = *v,
(ParticleField::DirectionY, FieldValue::Float(v)) => emitter.direction.y = *v,
(ParticleField::DirectionZ, FieldValue::Float(v)) => emitter.direction.z = *v,
(ParticleField::SpawnRate, FieldValue::Float(v)) => emitter.spawn_rate = *v,
(ParticleField::BurstCount, FieldValue::Float(v)) => {
emitter.burst_count = v.max(0.0) as u32
}
(ParticleField::LifetimeMin, FieldValue::Float(v)) => emitter.particle_lifetime_min = *v,
(ParticleField::LifetimeMax, FieldValue::Float(v)) => emitter.particle_lifetime_max = *v,
(ParticleField::VelocityMin, FieldValue::Float(v)) => emitter.initial_velocity_min = *v,
(ParticleField::VelocityMax, FieldValue::Float(v)) => emitter.initial_velocity_max = *v,
(ParticleField::VelocitySpread, FieldValue::Float(v)) => emitter.velocity_spread = *v,
(ParticleField::GravityX, FieldValue::Float(v)) => emitter.gravity.x = *v,
(ParticleField::GravityY, FieldValue::Float(v)) => emitter.gravity.y = *v,
(ParticleField::GravityZ, FieldValue::Float(v)) => emitter.gravity.z = *v,
(ParticleField::Drag, FieldValue::Float(v)) => emitter.drag = *v,
(ParticleField::SizeStart, FieldValue::Float(v)) => emitter.size_start = *v,
(ParticleField::SizeEnd, FieldValue::Float(v)) => emitter.size_end = *v,
(ParticleField::EmissiveStrength, FieldValue::Float(v)) => emitter.emissive_strength = *v,
(ParticleField::TurbulenceStrength, FieldValue::Float(v)) => {
emitter.turbulence_strength = *v
}
(ParticleField::TurbulenceFrequency, FieldValue::Float(v)) => {
emitter.turbulence_frequency = *v
}
(ParticleField::TextureIndex, FieldValue::Float(v)) => {
emitter.texture_index = v.max(0.0) as u32
}
(ParticleField::SizeCurveTime(index), FieldValue::Float(v)) => {
if let Some(key) = emitter.size_curve.get_mut(index) {
key.0 = (*v).clamp(0.0, 1.0);
}
}
(ParticleField::SizeCurveValue(index), FieldValue::Float(v)) => {
if let Some(key) = emitter.size_curve.get_mut(index) {
key.1 = *v;
}
}
(ParticleField::AddSizeCurveKey, FieldValue::Bool(true)) => {
emitter.size_curve.push((0.5, emitter.size_start));
}
(ParticleField::RemoveSizeCurveKey(index), FieldValue::Bool(true))
if index < emitter.size_curve.len() =>
{
emitter.size_curve.remove(index);
}
(ParticleField::OpacityCurveTime(index), FieldValue::Float(v)) => {
if let Some(key) = emitter.opacity_curve.get_mut(index) {
key.0 = (*v).clamp(0.0, 1.0);
}
}
(ParticleField::OpacityCurveValue(index), FieldValue::Float(v)) => {
if let Some(key) = emitter.opacity_curve.get_mut(index) {
key.1 = (*v).clamp(0.0, 1.0);
}
}
(ParticleField::AddOpacityCurveKey, FieldValue::Bool(true)) => {
emitter.opacity_curve.push((0.5, 1.0));
}
(ParticleField::RemoveOpacityCurveKey(index), FieldValue::Bool(true))
if index < emitter.opacity_curve.len() =>
{
emitter.opacity_curve.remove(index);
}
(ParticleField::DeathSubEmitter, FieldValue::Text(text)) => {
emitter.death_sub_emitter = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
(ParticleField::TrailSubEmitter, FieldValue::Text(text)) => {
emitter.trail_sub_emitter = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
(ParticleField::TrailSpawnInterval, FieldValue::Float(v)) => {
emitter.trail_spawn_interval = (*v).max(0.0);
}
_ => {}
}
}
fn apply_decal_change(world: &mut World, entity: Entity, field: DecalField, value: &FieldValue) {
let Some(decal) = world.core.get_decal_mut(entity) else {
return;
};
match (field, value) {
(DecalField::Color, FieldValue::Color(color)) => {
decal.color = [color.x, color.y, color.z, color.w];
}
(DecalField::EmissiveStrength, FieldValue::Float(v)) => decal.emissive_strength = *v,
(DecalField::SizeX, FieldValue::Float(v)) => decal.size.x = *v,
(DecalField::SizeY, FieldValue::Float(v)) => decal.size.y = *v,
(DecalField::Depth, FieldValue::Float(v)) => decal.depth = *v,
(DecalField::NormalThreshold, FieldValue::Float(v)) => decal.normal_threshold = *v,
(DecalField::FadeStart, FieldValue::Float(v)) => decal.fade_start = *v,
(DecalField::FadeEnd, FieldValue::Float(v)) => decal.fade_end = *v,
(DecalField::Texture, FieldValue::Text(text)) => {
decal.texture = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
(DecalField::EmissiveTexture, FieldValue::Text(text)) => {
decal.emissive_texture = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
_ => {}
}
}
fn apply_audio_change(world: &mut World, entity: Entity, field: AudioField, value: &FieldValue) {
use nightshade::ecs::audio::components::AudioBus;
let Some(source) = world.core.get_audio_source_mut(entity) else {
return;
};
match (field, value) {
(AudioField::AudioRef, FieldValue::Text(text)) => {
source.audio_ref = if text.is_empty() {
None
} else {
Some(text.clone())
};
}
(AudioField::Volume, FieldValue::Float(v)) => source.volume = *v,
(AudioField::Looping, FieldValue::Bool(v)) => source.looping = *v,
(AudioField::Playing, FieldValue::Bool(v)) => source.playing = *v,
(AudioField::Spatial, FieldValue::Bool(v)) => source.spatial = *v,
(AudioField::Bus, FieldValue::Index(index)) => {
source.bus = match index {
0 => AudioBus::Master,
1 => AudioBus::Music,
2 => AudioBus::Sfx,
3 => AudioBus::Ambient,
4 => AudioBus::Voice,
_ => AudioBus::Ui,
};
}
(AudioField::MinDistance, FieldValue::Float(v)) => source.min_distance = *v,
(AudioField::MaxDistance, FieldValue::Float(v)) => source.max_distance = *v,
(AudioField::ReverbZoneName(index), FieldValue::Text(text)) => {
if let Some(zone) = source.reverb_zones.get_mut(index) {
zone.0 = text.clone();
}
}
(AudioField::ReverbZoneSend(index), FieldValue::Float(value)) => {
if let Some(zone) = source.reverb_zones.get_mut(index) {
zone.1 = *value;
}
}
(AudioField::AddReverbZone, FieldValue::Bool(true)) => {
source
.reverb_zones
.push((format!("zone_{}", source.reverb_zones.len()), 0.0));
}
(AudioField::RemoveReverbZone(index), FieldValue::Bool(true))
if index < source.reverb_zones.len() =>
{
source.reverb_zones.remove(index);
}
(AudioField::RandomPick, FieldValue::Bool(value)) => {
source.random_pick = *value;
}
(AudioField::RandomClip(index), FieldValue::Text(text)) => {
if let Some(clip) = source.random_clips.get_mut(index) {
*clip = text.clone();
}
}
(AudioField::AddRandomClip, FieldValue::Bool(true)) => {
source.random_clips.push(String::new());
}
(AudioField::RemoveRandomClip(index), FieldValue::Bool(true))
if index < source.random_clips.len() =>
{
source.random_clips.remove(index);
}
_ => {}
}
}
fn apply_rigid_body_change(
world: &mut World,
entity: Entity,
field: RigidBodyField,
value: &FieldValue,
) {
use nightshade::ecs::physics::types::RigidBodyType;
let Some(body) = world.core.get_rigid_body_mut(entity) else {
return;
};
match (field, value) {
(RigidBodyField::BodyType, FieldValue::Index(index)) => {
body.body_type = match index {
0 => RigidBodyType::Dynamic,
1 => RigidBodyType::KinematicPositionBased,
2 => RigidBodyType::KinematicVelocityBased,
_ => RigidBodyType::Fixed,
};
}
(RigidBodyField::Mass, FieldValue::Float(v)) => body.mass = *v,
(RigidBodyField::LinvelX, FieldValue::Float(v)) => body.linvel[0] = *v,
(RigidBodyField::LinvelY, FieldValue::Float(v)) => body.linvel[1] = *v,
(RigidBodyField::LinvelZ, FieldValue::Float(v)) => body.linvel[2] = *v,
(RigidBodyField::AngvelX, FieldValue::Float(v)) => body.angvel[0] = *v,
(RigidBodyField::AngvelY, FieldValue::Float(v)) => body.angvel[1] = *v,
(RigidBodyField::AngvelZ, FieldValue::Float(v)) => body.angvel[2] = *v,
(RigidBodyField::Ccd, FieldValue::Bool(v)) => body.ccd_enabled = *v,
_ => {}
}
}
fn apply_collider_change(
world: &mut World,
entity: Entity,
field: ColliderField,
value: &FieldValue,
) {
if let (ColliderField::Shape, FieldValue::Index(index)) = (field, value) {
let new_shape = match index {
0 => ColliderShape::Ball { radius: 0.5 },
1 => ColliderShape::Cuboid {
hx: 0.5,
hy: 0.5,
hz: 0.5,
},
2 => ColliderShape::Capsule {
half_height: 0.5,
radius: 0.25,
},
3 => ColliderShape::Cylinder {
half_height: 0.5,
radius: 0.25,
},
4 => ColliderShape::Cone {
half_height: 0.5,
radius: 0.25,
},
5 => convex_mesh_from_entity(world, entity)
.unwrap_or(ColliderShape::ConvexMesh { vertices: vec![] }),
6 => trimesh_from_entity(world, entity).unwrap_or(ColliderShape::TriMesh {
vertices: vec![],
indices: vec![],
}),
7 => ColliderShape::HeightField {
nrows: 8,
ncols: 8,
heights: vec![0.0; 64],
scale: [1.0, 1.0, 1.0],
},
_ => return,
};
if let Some(collider) = world.core.get_collider_mut(entity) {
collider.shape = new_shape;
}
return;
}
let Some(collider) = world.core.get_collider_mut(entity) else {
return;
};
match (field, value) {
(ColliderField::Shape, _) => {}
(ColliderField::BallRadius, FieldValue::Float(v)) => {
if let ColliderShape::Ball { radius } = &mut collider.shape {
*radius = *v;
}
}
(ColliderField::CuboidX, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hx, .. } = &mut collider.shape {
*hx = *v;
}
}
(ColliderField::CuboidY, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hy, .. } = &mut collider.shape {
*hy = *v;
}
}
(ColliderField::CuboidZ, FieldValue::Float(v)) => {
if let ColliderShape::Cuboid { hz, .. } = &mut collider.shape {
*hz = *v;
}
}
(ColliderField::CapsuleHalfHeight, FieldValue::Float(v)) => {
if let ColliderShape::Capsule { half_height, .. } = &mut collider.shape {
*half_height = *v;
}
}
(ColliderField::CapsuleRadius, FieldValue::Float(v)) => {
if let ColliderShape::Capsule { radius, .. } = &mut collider.shape {
*radius = *v;
}
}
(ColliderField::CylinderHalfHeight, FieldValue::Float(v)) => {
if let ColliderShape::Cylinder { half_height, .. } = &mut collider.shape {
*half_height = *v;
}
}
(ColliderField::CylinderRadius, FieldValue::Float(v)) => {
if let ColliderShape::Cylinder { radius, .. } = &mut collider.shape {
*radius = *v;
}
}
(ColliderField::ConeHalfHeight, FieldValue::Float(v)) => {
if let ColliderShape::Cone { half_height, .. } = &mut collider.shape {
*half_height = *v;
}
}
(ColliderField::ConeRadius, FieldValue::Float(v)) => {
if let ColliderShape::Cone { radius, .. } = &mut collider.shape {
*radius = *v;
}
}
(ColliderField::Friction, FieldValue::Float(v)) => collider.friction = *v,
(ColliderField::Restitution, FieldValue::Float(v)) => collider.restitution = *v,
(ColliderField::Density, FieldValue::Float(v)) => collider.density = *v,
(ColliderField::IsSensor, FieldValue::Bool(v)) => collider.is_sensor = *v,
(ColliderField::CollisionMemberships, FieldValue::Float(v)) => {
collider.collision_groups.memberships = (*v).clamp(0.0, u32::MAX as f32) as u32;
}
(ColliderField::CollisionFilter, FieldValue::Float(v)) => {
collider.collision_groups.filter = (*v).clamp(0.0, u32::MAX as f32) as u32;
}
(ColliderField::SolverMemberships, FieldValue::Float(v)) => {
collider.solver_groups.memberships = (*v).clamp(0.0, u32::MAX as f32) as u32;
}
(ColliderField::SolverFilter, FieldValue::Float(v)) => {
collider.solver_groups.filter = (*v).clamp(0.0, u32::MAX as f32) as u32;
}
_ => {}
}
}
fn apply_navmesh_change(
world: &mut World,
entity: Entity,
field: NavmeshField,
value: &FieldValue,
) {
let Some(agent) = world.core.get_navmesh_agent_mut(entity) else {
return;
};
match (field, value) {
(NavmeshField::MovementSpeed, FieldValue::Float(v)) => agent.movement_speed = *v,
(NavmeshField::ArrivalThreshold, FieldValue::Float(v)) => agent.arrival_threshold = *v,
(NavmeshField::PathRecalculationThreshold, FieldValue::Float(v)) => {
agent.path_recalculation_threshold = *v
}
(NavmeshField::AgentRadius, FieldValue::Float(v)) => agent.agent_radius = *v,
(NavmeshField::AgentHeight, FieldValue::Float(v)) => agent.agent_height = *v,
_ => {}
}
}
fn apply_camera_change(world: &mut World, entity: Entity, field: CameraField, value: &FieldValue) {
use nightshade::ecs::camera::components::{OrthographicCamera, PerspectiveCamera, Projection};
if matches!(field, CameraField::SetActive) {
if let FieldValue::Bool(true) = value {
world.resources.active_camera = Some(entity);
}
return;
}
let Some(camera) = world.core.get_camera_mut(entity) else {
return;
};
match (field, value) {
(CameraField::ProjectionKind, FieldValue::Index(index)) => {
camera.projection = match index {
0 => Projection::Perspective(PerspectiveCamera::default()),
_ => Projection::Orthographic(OrthographicCamera::default()),
};
}
(CameraField::FovDegrees, FieldValue::Float(degrees)) => {
if let Projection::Perspective(perspective) = &mut camera.projection {
perspective.y_fov_rad = degrees.to_radians();
}
}
(CameraField::NearPlane, FieldValue::Float(v)) => {
if let Projection::Perspective(perspective) = &mut camera.projection {
perspective.z_near = *v;
}
}
(CameraField::FarOverride, FieldValue::Bool(v)) => {
if let Projection::Perspective(perspective) = &mut camera.projection {
perspective.z_far = if *v { Some(1000.0) } else { None };
}
}
(CameraField::FarValue, FieldValue::Float(v)) => {
if let Projection::Perspective(perspective) = &mut camera.projection
&& perspective.z_far.is_some()
{
perspective.z_far = Some(*v);
}
}
(CameraField::AspectOverride, FieldValue::Bool(v)) => {
if let Projection::Perspective(perspective) = &mut camera.projection {
perspective.aspect_ratio = if *v { Some(16.0 / 9.0) } else { None };
}
}
(CameraField::AspectValue, FieldValue::Float(v)) => {
if let Projection::Perspective(perspective) = &mut camera.projection
&& perspective.aspect_ratio.is_some()
{
perspective.aspect_ratio = Some(*v);
}
}
(CameraField::OrthoX, FieldValue::Float(v)) => {
if let Projection::Orthographic(ortho) = &mut camera.projection {
ortho.x_mag = *v;
}
}
(CameraField::OrthoY, FieldValue::Float(v)) => {
if let Projection::Orthographic(ortho) = &mut camera.projection {
ortho.y_mag = *v;
}
}
(CameraField::OrthoNear, FieldValue::Float(v)) => {
if let Projection::Orthographic(ortho) = &mut camera.projection {
ortho.z_near = *v;
}
}
(CameraField::OrthoFar, FieldValue::Float(v)) => {
if let Projection::Orthographic(ortho) = &mut camera.projection {
ortho.z_far = *v;
}
}
_ => {}
}
}
fn apply_animation_change(
world: &mut World,
entity: Entity,
field: AnimationField,
value: &FieldValue,
) {
let Some(player) = world.core.get_animation_player_mut(entity) else {
return;
};
match (field, value) {
(AnimationField::Playing, FieldValue::Bool(v)) => player.playing = *v,
(AnimationField::Looping, FieldValue::Bool(v)) => player.looping = *v,
(AnimationField::PlayAll, FieldValue::Bool(v)) => player.play_all = *v,
(AnimationField::Speed, FieldValue::Float(v)) => player.speed = *v,
(AnimationField::Time, FieldValue::Float(v)) => player.time = *v,
(AnimationField::ClipSelect, FieldValue::Index(index)) if *index < player.clips.len() => {
player.current_clip = Some(*index);
player.time = 0.0;
}
_ => {}
}
}
fn apply_material_change(
world: &mut World,
entity: Entity,
field: MaterialField,
value: &FieldValue,
) {
use nightshade::ecs::material::components::AlphaMode;
let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
return;
};
let mut material = nightshade::ecs::material::resources::material_registry_iter(
&world.resources.assets.material_registry,
)
.find(|(name, _)| name.as_str() == material_ref.name.as_str())
.map(|(_, material)| material.clone());
let Some(material) = material.as_mut() else {
return;
};
match (field, value) {
(MaterialField::BaseColor, FieldValue::Color(color)) => {
material.base_color = [color.x, color.y, color.z, color.w];
}
(MaterialField::Metallic, FieldValue::Float(v)) => material.metallic = *v,
(MaterialField::Roughness, FieldValue::Float(v)) => material.roughness = *v,
(MaterialField::EmissiveFactor, FieldValue::Color(color)) => {
material.emissive_factor = [color.x, color.y, color.z];
}
(MaterialField::EmissiveStrength, FieldValue::Float(v)) => material.emissive_strength = *v,
(MaterialField::AlphaMode, FieldValue::Index(index)) => {
material.alpha_mode = match index {
0 => AlphaMode::Opaque,
1 => AlphaMode::Mask,
_ => AlphaMode::Blend,
};
}
(MaterialField::AlphaCutoff, FieldValue::Float(v)) => material.alpha_cutoff = *v,
(MaterialField::Unlit, FieldValue::Bool(v)) => material.unlit = *v,
(MaterialField::DoubleSided, FieldValue::Bool(v)) => material.double_sided = *v,
(MaterialField::NormalScale, FieldValue::Float(v)) => material.normal_scale = *v,
(MaterialField::OcclusionStrength, FieldValue::Float(v)) => {
material.occlusion_strength = *v
}
(MaterialField::Ior, FieldValue::Float(v)) => material.ior = *v,
(MaterialField::Transmission, FieldValue::Float(v)) => material.transmission_factor = *v,
(MaterialField::Thickness, FieldValue::Float(v)) => material.thickness = *v,
(MaterialField::ClearcoatFactor, FieldValue::Float(v)) => material.clearcoat_factor = *v,
(MaterialField::ClearcoatRoughness, FieldValue::Float(v)) => {
material.clearcoat_roughness_factor = *v
}
(MaterialField::SheenColor, FieldValue::Color(color)) => {
material.sheen_color_factor = [color.x, color.y, color.z];
}
(MaterialField::SheenRoughness, FieldValue::Float(v)) => {
material.sheen_roughness_factor = *v
}
(MaterialField::IridescenceFactor, FieldValue::Float(v)) => {
material.iridescence_factor = *v
}
(MaterialField::AnisotropyStrength, FieldValue::Float(v)) => {
material.anisotropy_strength = *v
}
_ => return,
}
queue_ecs_command(
world,
nightshade::ecs::world::commands::EcsCommand::ReloadMaterial {
name: material_ref.name.clone(),
material: Box::new(material.clone()),
},
);
}
fn apply_text_change(world: &mut World, entity: Entity, field: TextField, value: &FieldValue) {
match (field, value) {
(TextField::Content, FieldValue::Text(content)) => {
let text_index = world.core.get_text(entity).map(|text| text.text_index);
if let Some(text_index) = text_index {
world
.resources
.text
.cache
.set_text(text_index, content.clone());
}
if let Some(text) = world.core.get_text_mut(entity) {
text.dirty = true;
}
}
(TextField::FontSize, FieldValue::Float(v)) => {
if let Some(text) = world.core.get_text_mut(entity) {
text.properties.font_size = *v;
text.dirty = true;
}
}
(TextField::Color, FieldValue::Color(color)) => {
if let Some(text) = world.core.get_text_mut(entity) {
text.properties.color = *color;
text.dirty = true;
}
}
_ => {}
}
}
fn apply_grass_change(world: &mut World, entity: Entity, field: GrassField, value: &FieldValue) {
let Some(region) = world.core.get_grass_region_mut(entity) else {
return;
};
match (field, value) {
(GrassField::Enabled, FieldValue::Bool(v)) => region.enabled = *v,
(GrassField::WindStrength, FieldValue::Float(v)) => region.config.wind_strength = *v,
(GrassField::WindFrequency, FieldValue::Float(v)) => region.config.wind_frequency = *v,
(GrassField::WindDirX, FieldValue::Float(v)) => region.config.wind_direction[0] = *v,
(GrassField::WindDirZ, FieldValue::Float(v)) => region.config.wind_direction[1] = *v,
(GrassField::InteractionRadius, FieldValue::Float(v)) => {
region.config.interaction_radius = *v
}
(GrassField::InteractionStrength, FieldValue::Float(v)) => {
region.config.interaction_strength = *v
}
(GrassField::CastShadows, FieldValue::Bool(v)) => region.config.cast_shadows = *v,
(GrassField::ReceiveShadows, FieldValue::Bool(v)) => region.config.receive_shadows = *v,
(GrassField::BladesPerPatch, FieldValue::Float(v)) => {
region.config.blades_per_patch = v.max(1.0) as u32;
}
(GrassField::PatchSize, FieldValue::Float(v)) => region.config.patch_size = *v,
(GrassField::StreamRadius, FieldValue::Float(v)) => {
region.config.stream_radius = *v;
region.config.unload_radius = *v + 20.0;
}
_ => {}
}
}
fn apply_grass_interactor_change(
world: &mut World,
entity: Entity,
field: GrassInteractorField,
value: &FieldValue,
) {
let Some(interactor) = world.core.get_grass_interactor_mut(entity) else {
return;
};
match (field, value) {
(GrassInteractorField::Radius, FieldValue::Float(v)) => interactor.radius = *v,
(GrassInteractorField::Strength, FieldValue::Float(v)) => interactor.strength = *v,
_ => {}
}
}
fn apply_misc_scene_change(
editor_world: &mut EditorWorld,
entity: Entity,
field: MiscField,
value: &FieldValue,
) -> bool {
use nightshade::ecs::scene::{ChunkId, LayerId, SceneChunkConfig, SceneLayerConfig};
match (field, value) {
(MiscField::SceneLayerAssignment, FieldValue::Index(index)) => {
let layer_id = layer_id_from_index(editor_world, *index);
editor_world
.resources
.editor_scene
.set_entity_layer(entity, layer_id);
sync_entity_after_scene_change(editor_world, entity);
true
}
(MiscField::SceneChunkAssignment, FieldValue::Index(index)) => {
let chunk_id = chunk_id_from_index(editor_world, *index);
editor_world
.resources
.editor_scene
.set_entity_chunk(entity, chunk_id);
sync_entity_after_scene_change(editor_world, entity);
true
}
(MiscField::SceneAddLayer, FieldValue::Bool(true)) => {
let next_id = next_layer_id(editor_world);
editor_world
.resources
.project
.scene
.layers
.push(SceneLayerConfig::new(next_id, format!("Layer {}", next_id)));
editor_world.resources.project.mark_modified();
true
}
(MiscField::SceneAddChunk, FieldValue::Bool(true)) => {
let next_id = next_chunk_id(editor_world);
editor_world
.resources
.project
.scene
.chunks
.push(SceneChunkConfig::new(
next_id,
format!("Chunk {}", next_id),
[-50.0, -50.0, -50.0],
[50.0, 50.0, 50.0],
));
editor_world.resources.project.mark_modified();
true
}
(MiscField::SceneRemoveLayer(layer_id), FieldValue::Bool(true)) => {
editor_world
.resources
.project
.scene
.layers
.retain(|layer| layer.id != LayerId(layer_id));
let removals: Vec<nightshade::prelude::Entity> = editor_world
.resources
.editor_scene
.entity_layers
.iter()
.filter(|(_, layer)| **layer == LayerId(layer_id))
.map(|(entity, _)| *entity)
.collect();
for entity in removals {
editor_world
.resources
.editor_scene
.set_entity_layer(entity, None);
}
editor_world.resources.project.mark_modified();
true
}
(MiscField::SceneRemoveChunk(chunk_id), FieldValue::Bool(true)) => {
editor_world
.resources
.project
.scene
.chunks
.retain(|chunk| chunk.id != ChunkId(chunk_id));
let removals: Vec<nightshade::prelude::Entity> = editor_world
.resources
.editor_scene
.entity_chunks
.iter()
.filter(|(_, chunk)| **chunk == ChunkId(chunk_id))
.map(|(entity, _)| *entity)
.collect();
for entity in removals {
editor_world
.resources
.editor_scene
.set_entity_chunk(entity, None);
}
editor_world.resources.project.mark_modified();
true
}
(MiscField::SceneLayerName(layer_id), FieldValue::Text(text)) => {
if let Some(layer) = editor_world
.resources
.project
.scene
.layers
.iter_mut()
.find(|layer| layer.id == LayerId(layer_id))
{
layer.name = text.clone();
editor_world.resources.project.mark_modified();
}
true
}
(MiscField::SceneLayerOrder(layer_id), FieldValue::Float(value)) => {
if let Some(layer) = editor_world
.resources
.project
.scene
.layers
.iter_mut()
.find(|layer| layer.id == LayerId(layer_id))
{
layer.order = *value as i32;
editor_world.resources.project.mark_modified();
}
true
}
(MiscField::SceneLayerEnabled(layer_id), FieldValue::Bool(value)) => {
if let Some(layer) = editor_world
.resources
.project
.scene
.layers
.iter_mut()
.find(|layer| layer.id == LayerId(layer_id))
{
layer.enabled = *value;
editor_world.resources.project.mark_modified();
}
true
}
(MiscField::SceneChunkName(chunk_id), FieldValue::Text(text)) => {
if let Some(chunk) = editor_world
.resources
.project
.scene
.chunks
.iter_mut()
.find(|chunk| chunk.id == ChunkId(chunk_id))
{
chunk.name = text.clone();
editor_world.resources.project.mark_modified();
}
true
}
(MiscField::SceneChunkLoadDistance(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.load_distance = *value);
true
}
(MiscField::SceneChunkUnloadDistance(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| {
chunk.unload_distance = *value
});
true
}
(MiscField::SceneChunkBoundsMinX(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[0] = *value);
true
}
(MiscField::SceneChunkBoundsMinY(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[1] = *value);
true
}
(MiscField::SceneChunkBoundsMinZ(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[2] = *value);
true
}
(MiscField::SceneChunkBoundsMaxX(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[0] = *value);
true
}
(MiscField::SceneChunkBoundsMaxY(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[1] = *value);
true
}
(MiscField::SceneChunkBoundsMaxZ(chunk_id), FieldValue::Float(value)) => {
update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[2] = *value);
true
}
(MiscField::AddFixedJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Fixed);
true
}
(MiscField::AddRevoluteJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Revolute);
true
}
(MiscField::AddPrismaticJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Prismatic);
true
}
(MiscField::AddSphericalJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Spherical);
true
}
(MiscField::AddRopeJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Rope);
true
}
(MiscField::AddSpringJoint, FieldValue::Bool(true)) => {
add_joint_from_selection(editor_world, JointKind::Spring);
true
}
(MiscField::RemoveJoint(index), FieldValue::Bool(true))
if index < editor_world.resources.project.scene.joints.len() =>
{
editor_world.resources.project.scene.joints.remove(index);
editor_world.resources.project.mark_modified();
true
}
(MiscField::ToggleJointCollisions(index), FieldValue::Bool(value)) => {
if let Some(joint) = editor_world.resources.project.scene.joints.get_mut(index) {
joint.collisions_enabled = *value;
editor_world.resources.project.mark_modified();
}
true
}
_ => false,
}
}
#[derive(Clone, Copy)]
enum JointKind {
Fixed,
Revolute,
Prismatic,
Spherical,
Rope,
Spring,
}
fn add_joint_from_selection(editor_world: &mut EditorWorld, kind: JointKind) {
use nightshade::ecs::scene::{SceneJoint, SceneJointConnection};
let selected = editor_world.resources.ui.selected_entities.clone();
if selected.len() < 2 {
return;
}
let parent_entity = selected[selected.len() - 2];
let child_entity = selected[selected.len() - 1];
let parent_uuid = editor_world.resources.editor_scene.uuid_for(parent_entity);
let child_uuid = editor_world.resources.editor_scene.uuid_for(child_entity);
let (Some(parent_uuid), Some(child_uuid)) = (parent_uuid, child_uuid) else {
return;
};
let joint = match kind {
JointKind::Fixed => SceneJoint::Fixed {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
},
JointKind::Revolute => SceneJoint::Revolute {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
axis: [0.0, 1.0, 0.0],
limits: None,
},
JointKind::Prismatic => SceneJoint::Prismatic {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
axis: [1.0, 0.0, 0.0],
limits: None,
},
JointKind::Spherical => SceneJoint::Spherical {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
},
JointKind::Rope => SceneJoint::Rope {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
max_distance: 1.0,
},
JointKind::Spring => SceneJoint::Spring {
parent_anchor: [0.0, 0.0, 0.0],
child_anchor: [0.0, 0.0, 0.0],
rest_length: 1.0,
stiffness: 10.0,
damping: 1.0,
},
};
editor_world
.resources
.project
.scene
.joints
.push(SceneJointConnection {
parent_entity: parent_uuid,
child_entity: child_uuid,
joint,
collisions_enabled: false,
});
editor_world.resources.project.mark_modified();
}
fn next_layer_id(editor_world: &EditorWorld) -> u32 {
editor_world
.resources
.project
.scene
.layers
.iter()
.map(|layer| layer.id.0)
.max()
.map(|value| value + 1)
.unwrap_or(0)
}
fn next_chunk_id(editor_world: &EditorWorld) -> u32 {
editor_world
.resources
.project
.scene
.chunks
.iter()
.map(|chunk| chunk.id.0)
.max()
.map(|value| value + 1)
.unwrap_or(0)
}
fn layer_id_from_index(
editor_world: &EditorWorld,
index: usize,
) -> Option<nightshade::ecs::scene::LayerId> {
if index == 0 {
return None;
}
editor_world
.resources
.project
.scene
.layers
.get(index - 1)
.map(|layer| layer.id)
}
fn chunk_id_from_index(
editor_world: &EditorWorld,
index: usize,
) -> Option<nightshade::ecs::scene::ChunkId> {
if index == 0 {
return None;
}
editor_world
.resources
.project
.scene
.chunks
.get(index - 1)
.map(|chunk| chunk.id)
}
fn update_chunk<F: FnOnce(&mut nightshade::ecs::scene::SceneChunkConfig)>(
editor_world: &mut EditorWorld,
chunk_id: u32,
apply: F,
) {
use nightshade::ecs::scene::ChunkId;
if let Some(chunk) = editor_world
.resources
.project
.scene
.chunks
.iter_mut()
.find(|chunk| chunk.id == ChunkId(chunk_id))
{
apply(chunk);
editor_world.resources.project.mark_modified();
}
}
fn sync_entity_after_scene_change(editor_world: &mut EditorWorld, entity: Entity) {
let project = &mut editor_world.resources.project;
let editor_scene = &editor_world.resources.editor_scene;
let Some(uuid) = editor_scene.uuid_for(entity) else {
return;
};
let Some(scene_entity) = project
.scene
.entities
.iter_mut()
.find(|scene_entity| scene_entity.uuid == uuid)
else {
return;
};
scene_entity.layer = editor_scene.entity_layer(entity);
scene_entity.chunk_id = editor_scene.entity_chunk(entity);
project.mark_modified();
}
fn apply_misc_change(world: &mut World, entity: Entity, field: MiscField, value: &FieldValue) {
match (field, value) {
(MiscField::RenderLayer, FieldValue::Index(index)) => {
let value = if *index == 1 {
nightshade::ecs::primitives::RenderLayer::OVERLAY
} else {
nightshade::ecs::primitives::RenderLayer::WORLD
};
world
.core
.set_render_layer(entity, nightshade::ecs::primitives::RenderLayer(value));
}
(MiscField::CullingMaskBit(bit), FieldValue::Bool(set)) => {
if let Some(mask) = world.core.get_culling_mask_mut(entity) {
if *set {
mask.0 |= 1u32 << bit;
} else {
mask.0 &= !(1u32 << bit);
}
}
}
(MiscField::CameraCullingMaskBit(bit), FieldValue::Bool(set)) => {
if let Some(mask) = world.core.get_camera_culling_mask_mut(entity) {
if *set {
mask.0 |= 1u32 << bit;
} else {
mask.0 &= !(1u32 << bit);
}
}
}
(MiscField::IgnoreParentScale, FieldValue::Bool(false)) => {
world
.core
.remove_components(entity, nightshade::ecs::world::IGNORE_PARENT_SCALE);
}
(MiscField::LinesAlwaysOnTop, FieldValue::Bool(value)) => {
if let Some(lines) = world.core.get_lines_mut(entity) {
lines.always_on_top = *value;
lines.mark_dirty();
}
}
(MiscField::LinesClear, FieldValue::Bool(true)) => {
if let Some(lines) = world.core.get_lines_mut(entity) {
lines.clear();
}
}
_ => {}
}
}
const MAX_VISIBLE_INSTANCE_ROWS: usize = 64;
fn populate_instanced_mesh_section(
tree: &mut UiTreeBuilder,
grid: Entity,
entity: Entity,
bindings: &mut HashMap<Entity, InspectorBinding>,
) {
let Some(instanced) = tree.world_mut().core.get_instanced_mesh(entity) else {
return;
};
let mesh_name = instanced.mesh_name.clone();
let instance_count = instanced.instances.len();
let visible_count = instance_count.min(MAX_VISIBLE_INSTANCE_ROWS);
let instances: Vec<nightshade::ecs::mesh::components::InstanceTransform> = instanced
.instances
.iter()
.take(visible_count)
.copied()
.collect();
let custom_data: Vec<[f32; 4]> = (0..visible_count)
.map(|index| {
instanced
.custom_data
.get(index)
.map(|data| data.tint)
.unwrap_or([1.0, 1.0, 1.0, 1.0])
})
.collect();
let section = tree.add_property_section(grid, "Instanced mesh");
add_property_value(tree, grid, section, "Mesh", &mesh_name);
add_property_value(
tree,
grid,
section,
"Instance count",
&instance_count.to_string(),
);
let add_button = add_property_button(tree, grid, section, "Add instance", "Add");
bindings.insert(
add_button,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::Add),
},
);
for (index, transform) in instances.iter().enumerate() {
let header = format!("Instance {index}");
let instance_section = tree.add_property_section(grid, &header);
let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);
let tx = add_property_drag_value(
tree,
grid,
instance_section,
"Translation X",
transform.translation.x,
0.05,
3,
);
bindings.insert(
tx,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::TranslationX(index)),
},
);
let ty = add_property_drag_value(
tree,
grid,
instance_section,
"Translation Y",
transform.translation.y,
0.05,
3,
);
bindings.insert(
ty,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::TranslationY(index)),
},
);
let tz = add_property_drag_value(
tree,
grid,
instance_section,
"Translation Z",
transform.translation.z,
0.05,
3,
);
bindings.insert(
tz,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::TranslationZ(index)),
},
);
let rx = add_property_drag_value(
tree,
grid,
instance_section,
"Rotation X (\u{00b0})",
roll.to_degrees(),
0.5,
1,
);
bindings.insert(
rx,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::EulerX(index)),
},
);
let ry = add_property_drag_value(
tree,
grid,
instance_section,
"Rotation Y (\u{00b0})",
pitch.to_degrees(),
0.5,
1,
);
bindings.insert(
ry,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::EulerY(index)),
},
);
let rz = add_property_drag_value(
tree,
grid,
instance_section,
"Rotation Z (\u{00b0})",
yaw.to_degrees(),
0.5,
1,
);
bindings.insert(
rz,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::EulerZ(index)),
},
);
let sx = add_property_drag_value(
tree,
grid,
instance_section,
"Scale X",
transform.scale.x,
0.01,
3,
);
bindings.insert(
sx,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::ScaleX(index)),
},
);
let sy = add_property_drag_value(
tree,
grid,
instance_section,
"Scale Y",
transform.scale.y,
0.01,
3,
);
bindings.insert(
sy,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::ScaleY(index)),
},
);
let sz = add_property_drag_value(
tree,
grid,
instance_section,
"Scale Z",
transform.scale.z,
0.01,
3,
);
bindings.insert(
sz,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::ScaleZ(index)),
},
);
let tint = custom_data[index];
let tint_picker = add_property_color_picker(
tree,
grid,
instance_section,
"Tint",
vec4(tint[0], tint[1], tint[2], tint[3]),
);
bindings.insert(
tint_picker,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::Tint(index)),
},
);
let remove_button =
add_property_button(tree, grid, instance_section, "Remove instance", "Remove");
bindings.insert(
remove_button,
InspectorBinding {
entity,
field: InspectorField::Instance(InstanceField::Remove(index)),
},
);
}
if instance_count > visible_count {
let hidden = instance_count - visible_count;
let message = format!("+{hidden} more instances (hidden)");
add_property_value(tree, grid, section, "Hidden", &message);
}
}
fn apply_instanced_mesh_change(
world: &mut World,
entity: Entity,
field: InstanceField,
value: &FieldValue,
) -> bool {
let Some(instanced) = world.core.get_instanced_mesh_mut(entity) else {
return false;
};
match (field, value) {
(InstanceField::Add, FieldValue::Bool(true)) => {
instanced.add_instance(nightshade::ecs::mesh::components::InstanceTransform::default());
true
}
(InstanceField::Remove(index), FieldValue::Bool(true)) => {
instanced.remove_instance(index);
true
}
(
InstanceField::TranslationX(index)
| InstanceField::TranslationY(index)
| InstanceField::TranslationZ(index),
FieldValue::Float(v),
) => {
if let Some(existing) = instanced.get_instance(index).copied() {
let mut updated = existing;
match field {
InstanceField::TranslationX(_) => updated.translation.x = *v,
InstanceField::TranslationY(_) => updated.translation.y = *v,
InstanceField::TranslationZ(_) => updated.translation.z = *v,
_ => unreachable!(),
}
instanced.set_instance_transform(index, updated);
}
false
}
(
InstanceField::EulerX(index)
| InstanceField::EulerY(index)
| InstanceField::EulerZ(index),
FieldValue::Float(degrees),
) => {
if let Some(existing) = instanced.get_instance(index).copied() {
let mut updated = existing;
let (mut roll, mut pitch, mut yaw) = quat_to_euler_xyz(&updated.rotation);
let radians = degrees.to_radians();
match field {
InstanceField::EulerX(_) => roll = radians,
InstanceField::EulerY(_) => pitch = radians,
InstanceField::EulerZ(_) => yaw = radians,
_ => unreachable!(),
}
updated.rotation = euler_xyz_to_quat(roll, pitch, yaw);
instanced.set_instance_transform(index, updated);
}
false
}
(
InstanceField::ScaleX(index)
| InstanceField::ScaleY(index)
| InstanceField::ScaleZ(index),
FieldValue::Float(v),
) => {
if let Some(existing) = instanced.get_instance(index).copied() {
let mut updated = existing;
let clamped = v.max(0.001);
match field {
InstanceField::ScaleX(_) => updated.scale.x = clamped,
InstanceField::ScaleY(_) => updated.scale.y = clamped,
InstanceField::ScaleZ(_) => updated.scale.z = clamped,
_ => unreachable!(),
}
instanced.set_instance_transform(index, updated);
}
false
}
(InstanceField::Tint(index), FieldValue::Color(color)) => {
instanced.set_instance_tint(index, [color.x, color.y, color.z, color.w]);
false
}
_ => false,
}
}
fn collider_mesh_data(world: &World, entity: Entity) -> Option<(Vec<[f32; 3]>, Vec<u32>)> {
let mesh_name = world.core.get_render_mesh(entity)?.name.clone();
let registry = &world.resources.assets.mesh_cache.registry;
let mesh =
nightshade::ecs::generational_registry::registry_entry_by_name(registry, &mesh_name)?;
let vertices: Vec<[f32; 3]> = mesh.vertices.iter().map(|vertex| vertex.position).collect();
let indices = mesh.indices.clone();
Some((vertices, indices))
}
fn trimesh_from_entity(world: &World, entity: Entity) -> Option<ColliderShape> {
let (vertices, indices_flat) = collider_mesh_data(world, entity)?;
if vertices.is_empty() || indices_flat.len() < 3 {
return None;
}
let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(indices_flat.len() / 3);
for triangle in indices_flat.chunks_exact(3) {
triangles.push([triangle[0], triangle[1], triangle[2]]);
}
Some(ColliderShape::TriMesh {
vertices,
indices: triangles,
})
}
fn convex_mesh_from_entity(world: &World, entity: Entity) -> Option<ColliderShape> {
let (vertices, _) = collider_mesh_data(world, entity)?;
if vertices.is_empty() {
return None;
}
Some(ColliderShape::ConvexMesh { vertices })
}