feat(galaxy): refactor params into core/disk/beam layers, fix randomize button scaling
- Split GalaxyParams into CoreParams, DiskParams, and BeamParams sub-structs - Add polar beam systems (top/bottom) with visual differentiation - Add scrollable control panel with section headers for each layer - Fix Randomize Settings button: remove explicit width (use flex auto) and bump height from 28px to 32px to match other action buttons - Add mouse-wheel scroll routing for the control panel - Add 'Randomize Settings' button that randomizes all params within bounds - Add SystemOrigin enum for per-layer tracking - Update AGENTS.md with new module layout and plugin pattern
This commit is contained in:
@@ -4,8 +4,12 @@
|
||||
//! Bevy 0.16 does not ship a native Slider widget, so each parameter is a
|
||||
//! row of `Label Value [-] [+]` buttons. Each `+/-` button carries a
|
||||
//! [`ParamButton`] marker identifying which field to mutate.
|
||||
//!
|
||||
//! Rows are grouped under section headers (Galaxy, Core, Disk, Top Beam,
|
||||
//! Bottom Beam) for visual clarity now that the panel spans five conceptual
|
||||
//! layers.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::{input::mouse::MouseWheel, prelude::*, ui::ScrollPosition, window::PrimaryWindow};
|
||||
|
||||
use super::GalaxySpawned;
|
||||
use crate::gameplay::galaxy::params::*;
|
||||
@@ -15,6 +19,10 @@ use crate::gameplay::galaxy::params::*;
|
||||
#[derive(Component)]
|
||||
pub struct GalaxyControlPanel;
|
||||
|
||||
/// Marker for the scrollable inner container of the control panel.
|
||||
#[derive(Component)]
|
||||
pub struct GalaxyScrollContent;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct GalaxyInfoPanel;
|
||||
|
||||
@@ -23,18 +31,46 @@ pub struct GalaxyInfoPanel;
|
||||
pub enum ParamButton {
|
||||
SeedDecr,
|
||||
SeedIncr,
|
||||
CountDecr,
|
||||
CountIncr,
|
||||
ArmsDecr,
|
||||
ArmsIncr,
|
||||
VerticalArmsDecr,
|
||||
VerticalArmsIncr,
|
||||
SizeDecr,
|
||||
SizeIncr,
|
||||
TwistDecr,
|
||||
TwistIncr,
|
||||
VerticalTwistDecr,
|
||||
VerticalTwistIncr,
|
||||
// Core
|
||||
CoreCountDecr,
|
||||
CoreCountIncr,
|
||||
CoreRadiusDecr,
|
||||
CoreRadiusIncr,
|
||||
// Disk
|
||||
DiskCountDecr,
|
||||
DiskCountIncr,
|
||||
DiskArmsDecr,
|
||||
DiskArmsIncr,
|
||||
DiskTwistDecr,
|
||||
DiskTwistIncr,
|
||||
// Top beam (+Y)
|
||||
BeamTopEnabled,
|
||||
BeamTopThicknessDecr,
|
||||
BeamTopThicknessIncr,
|
||||
BeamTopLengthDecr,
|
||||
BeamTopLengthIncr,
|
||||
BeamTopDensityDecr,
|
||||
BeamTopDensityIncr,
|
||||
BeamTopTaperDecr,
|
||||
BeamTopTaperIncr,
|
||||
BeamTopGravityDecr,
|
||||
BeamTopGravityIncr,
|
||||
// Bottom beam (-Y)
|
||||
BeamBottomEnabled,
|
||||
BeamBottomThicknessDecr,
|
||||
BeamBottomThicknessIncr,
|
||||
BeamBottomLengthDecr,
|
||||
BeamBottomLengthIncr,
|
||||
BeamBottomDensityDecr,
|
||||
BeamBottomDensityIncr,
|
||||
BeamBottomTaperDecr,
|
||||
BeamBottomTaperIncr,
|
||||
BeamBottomGravityDecr,
|
||||
BeamBottomGravityIncr,
|
||||
/// Randomize all params to random valid values within their bounds.
|
||||
RandomSettings,
|
||||
/// Randomize: bump seed by 1 (equivalent to the docs "Regenerate" button).
|
||||
Regenerate,
|
||||
/// Reset orbit camera to default orientation.
|
||||
@@ -47,11 +83,13 @@ const PANEL_BG: Color = Color::srgb(0.05, 0.07, 0.12);
|
||||
const PANEL_BORDER: Color = Color::srgb(0.25, 0.40, 0.62);
|
||||
const TEXT_BRIGHT: Color = Color::srgb(0.82, 0.90, 1.0);
|
||||
const TEXT_DIM: Color = Color::srgb(0.55, 0.65, 0.82);
|
||||
const SECTION_TEXT: Color = Color::srgb(0.42, 0.62, 0.86);
|
||||
const BUTTON_BG: Color = Color::srgb(0.10, 0.14, 0.22);
|
||||
|
||||
const PANEL_WIDTH: f32 = 280.0;
|
||||
const PANEL_WIDTH: f32 = 300.0;
|
||||
const PANEL_PADDING: f32 = 14.0;
|
||||
const TITLE_FONT_SIZE: f32 = 20.0;
|
||||
const SECTION_FONT_SIZE: f32 = 12.0;
|
||||
const LABEL_FONT_SIZE: f32 = 15.0;
|
||||
const VALUE_FONT_SIZE: f32 = 15.0;
|
||||
const BUTTON_FONT_SIZE: f32 = 16.0;
|
||||
@@ -71,10 +109,9 @@ fn spawn_control_panel(commands: &mut Commands) {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(12.0),
|
||||
top: Val::Px(12.0),
|
||||
bottom: Val::Px(12.0),
|
||||
width: Val::Px(PANEL_WIDTH),
|
||||
padding: UiRect::all(Val::Px(PANEL_PADDING)),
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(6.0),
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
@@ -85,6 +122,7 @@ fn spawn_control_panel(commands: &mut Commands) {
|
||||
GalaxySpawned,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
// Sticky title — pinned above the scroll area.
|
||||
parent.spawn((
|
||||
Text::new("Galaxy Parameters"),
|
||||
TextFont {
|
||||
@@ -93,29 +131,18 @@ fn spawn_control_panel(commands: &mut Commands) {
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
Node {
|
||||
margin: UiRect::bottom(Val::Px(8.0)),
|
||||
padding: UiRect::px(PANEL_PADDING, PANEL_PADDING, PANEL_PADDING, 4.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Current params are read at button-press time in `param_button_handler`,
|
||||
// so we don't need to thread `Res<GalaxyParams>` through setup.
|
||||
spawn_param_row(parent, "Seed", "seed", ParamButton::SeedDecr, ParamButton::SeedIncr);
|
||||
spawn_param_row(parent, "Systems", "count", ParamButton::CountDecr, ParamButton::CountIncr);
|
||||
spawn_param_row(parent, "Disk Arms", "arms", ParamButton::ArmsDecr, ParamButton::ArmsIncr);
|
||||
spawn_param_row(parent, "Vertical Arms", "varms", ParamButton::VerticalArmsDecr, ParamButton::VerticalArmsIncr);
|
||||
spawn_param_row(parent, "Size", "size", ParamButton::SizeDecr, ParamButton::SizeIncr);
|
||||
spawn_param_row(parent, "Twist", "twist", ParamButton::TwistDecr, ParamButton::TwistIncr);
|
||||
spawn_param_row(parent, "Vertical Twist", "vtwist", ParamButton::VerticalTwistDecr, ParamButton::VerticalTwistIncr);
|
||||
|
||||
// Regenerate (randomize seed) button.
|
||||
// Randomize button — pinned above the scroll area.
|
||||
parent
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(32.0),
|
||||
margin: UiRect::top(Val::Px(8.0)),
|
||||
margin: UiRect::px(PANEL_PADDING, PANEL_PADDING, 4.0, PANEL_PADDING),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
@@ -124,11 +151,11 @@ fn spawn_control_panel(commands: &mut Commands) {
|
||||
BackgroundColor(BUTTON_BG),
|
||||
BorderColor(PANEL_BORDER),
|
||||
BorderRadius::all(Val::Px(6.0)),
|
||||
ParamButton::Regenerate,
|
||||
ParamButton::RandomSettings,
|
||||
))
|
||||
.with_children(|btn| {
|
||||
btn.spawn((
|
||||
Text::new("Regenerate"),
|
||||
Text::new("Randomize Settings"),
|
||||
TextFont {
|
||||
font_size: BUTTON_FONT_SIZE,
|
||||
..default()
|
||||
@@ -137,51 +164,243 @@ fn spawn_control_panel(commands: &mut Commands) {
|
||||
));
|
||||
});
|
||||
|
||||
// Center View (reset orbit camera) button.
|
||||
// Scrollable content area.
|
||||
parent
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(32.0),
|
||||
margin: UiRect::top(Val::Px(4.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
flex_grow: 1.0,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Val::Px(6.0),
|
||||
padding: UiRect::px(PANEL_PADDING, PANEL_PADDING, PANEL_PADDING, PANEL_PADDING),
|
||||
overflow: Overflow::scroll_y(),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BUTTON_BG),
|
||||
BorderColor(PANEL_BORDER),
|
||||
BorderRadius::all(Val::Px(6.0)),
|
||||
ParamButton::CenterView,
|
||||
ScrollPosition::default(),
|
||||
GalaxyScrollContent,
|
||||
))
|
||||
.with_children(|btn| {
|
||||
btn.spawn((
|
||||
Text::new("Center View"),
|
||||
.with_children(|scroll| {
|
||||
// ── Galaxy (seed + size) ────────────────────────────────
|
||||
spawn_section(scroll, "Galaxy");
|
||||
spawn_param_row(scroll, "Seed", "seed", ParamButton::SeedDecr, ParamButton::SeedIncr);
|
||||
spawn_param_row(scroll, "Size", "size", ParamButton::SizeDecr, ParamButton::SizeIncr);
|
||||
|
||||
// ── Core ────────────────────────────────────────────────
|
||||
spawn_section(scroll, "Core");
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Core Count",
|
||||
"core_count",
|
||||
ParamButton::CoreCountDecr,
|
||||
ParamButton::CoreCountIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Core Radius",
|
||||
"core_radius",
|
||||
ParamButton::CoreRadiusDecr,
|
||||
ParamButton::CoreRadiusIncr,
|
||||
);
|
||||
|
||||
// ── Disk ────────────────────────────────────────────────
|
||||
spawn_section(scroll, "Disk");
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Disk Count",
|
||||
"disk_count",
|
||||
ParamButton::DiskCountDecr,
|
||||
ParamButton::DiskCountIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Disk Arms",
|
||||
"disk_arms",
|
||||
ParamButton::DiskArmsDecr,
|
||||
ParamButton::DiskArmsIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Disk Twist",
|
||||
"disk_twist",
|
||||
ParamButton::DiskTwistDecr,
|
||||
ParamButton::DiskTwistIncr,
|
||||
);
|
||||
|
||||
// ── Top Beam (+Y) ───────────────────────────────────────
|
||||
spawn_section(scroll, "Top Beam (+Y)");
|
||||
spawn_toggle_row(scroll, "Enabled", "beam_top_enabled", ParamButton::BeamTopEnabled);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Thickness",
|
||||
"beam_top_thickness",
|
||||
ParamButton::BeamTopThicknessDecr,
|
||||
ParamButton::BeamTopThicknessIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Length",
|
||||
"beam_top_length",
|
||||
ParamButton::BeamTopLengthDecr,
|
||||
ParamButton::BeamTopLengthIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Density",
|
||||
"beam_top_density",
|
||||
ParamButton::BeamTopDensityDecr,
|
||||
ParamButton::BeamTopDensityIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Taper",
|
||||
"beam_top_taper",
|
||||
ParamButton::BeamTopTaperDecr,
|
||||
ParamButton::BeamTopTaperIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Gravity",
|
||||
"beam_top_gravity",
|
||||
ParamButton::BeamTopGravityDecr,
|
||||
ParamButton::BeamTopGravityIncr,
|
||||
);
|
||||
|
||||
// ── Bottom Beam (-Y) ────────────────────────────────────
|
||||
spawn_section(scroll, "Bottom Beam (-Y)");
|
||||
spawn_toggle_row(
|
||||
scroll,
|
||||
"Enabled",
|
||||
"beam_bottom_enabled",
|
||||
ParamButton::BeamBottomEnabled,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Thickness",
|
||||
"beam_bottom_thickness",
|
||||
ParamButton::BeamBottomThicknessDecr,
|
||||
ParamButton::BeamBottomThicknessIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Length",
|
||||
"beam_bottom_length",
|
||||
ParamButton::BeamBottomLengthDecr,
|
||||
ParamButton::BeamBottomLengthIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Density",
|
||||
"beam_bottom_density",
|
||||
ParamButton::BeamBottomDensityDecr,
|
||||
ParamButton::BeamBottomDensityIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Taper",
|
||||
"beam_bottom_taper",
|
||||
ParamButton::BeamBottomTaperDecr,
|
||||
ParamButton::BeamBottomTaperIncr,
|
||||
);
|
||||
spawn_param_row(
|
||||
scroll,
|
||||
"Gravity",
|
||||
"beam_bottom_gravity",
|
||||
ParamButton::BeamBottomGravityDecr,
|
||||
ParamButton::BeamBottomGravityIncr,
|
||||
);
|
||||
|
||||
// ── Action buttons ──────────────────────────────────────
|
||||
|
||||
// Regenerate (randomize seed) button.
|
||||
scroll
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(32.0),
|
||||
margin: UiRect::top(Val::Px(8.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BUTTON_BG),
|
||||
BorderColor(PANEL_BORDER),
|
||||
BorderRadius::all(Val::Px(6.0)),
|
||||
ParamButton::Regenerate,
|
||||
))
|
||||
.with_children(|btn| {
|
||||
btn.spawn((
|
||||
Text::new("Regenerate"),
|
||||
TextFont {
|
||||
font_size: BUTTON_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
});
|
||||
|
||||
// Center View (reset orbit camera) button.
|
||||
scroll
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Px(32.0),
|
||||
margin: UiRect::top(Val::Px(4.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BUTTON_BG),
|
||||
BorderColor(PANEL_BORDER),
|
||||
BorderRadius::all(Val::Px(6.0)),
|
||||
ParamButton::CenterView,
|
||||
))
|
||||
.with_children(|btn| {
|
||||
btn.spawn((
|
||||
Text::new("Center View"),
|
||||
TextFont {
|
||||
font_size: BUTTON_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
});
|
||||
|
||||
// Help text.
|
||||
scroll.spawn((
|
||||
Text::new("Drag to orbit (360°) • Scroll to zoom • Click a star • Esc to return"),
|
||||
TextFont {
|
||||
font_size: BUTTON_FONT_SIZE,
|
||||
font_size: HELP_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
Node {
|
||||
margin: UiRect::top(Val::Px(10.0)),
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
});
|
||||
|
||||
// Help text.
|
||||
parent.spawn((
|
||||
Text::new("Drag to orbit (360°) • Scroll to zoom • Click a star • Esc to return"),
|
||||
TextFont {
|
||||
font_size: HELP_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
Node {
|
||||
margin: UiRect::top(Val::Px(10.0)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Section header row — small dim label with a top margin to separate groups.
|
||||
fn spawn_section(parent: &mut ChildSpawnerCommands, label: &str) {
|
||||
parent.spawn((
|
||||
Text::new(label.to_uppercase()),
|
||||
TextFont {
|
||||
font_size: SECTION_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(SECTION_TEXT),
|
||||
Node {
|
||||
margin: UiRect::top(Val::Px(4.0)),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// Build a `[Label ........ value [-] [+]]` row. The `value_key` is stored in
|
||||
/// the marker as a label for the value text child; the actual value is read
|
||||
/// from `GalaxyParams` at button-press time and updated by
|
||||
@@ -211,7 +430,7 @@ fn spawn_param_row(
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
Node {
|
||||
width: Val::Px(118.0),
|
||||
width: Val::Px(128.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
@@ -238,17 +457,74 @@ fn spawn_param_row(
|
||||
});
|
||||
}
|
||||
|
||||
/// `[Label ........ value [ø]]` — a single-button row for boolean toggles
|
||||
/// (Enabled / Disabled). The button cycles the field on each press; the value
|
||||
/// text displays "On"/"Off" via [`refresh_control_panel_values`].
|
||||
fn spawn_toggle_row(
|
||||
parent: &mut ChildSpawnerCommands,
|
||||
label: &str,
|
||||
value_key: &str,
|
||||
toggle: ParamButton,
|
||||
) {
|
||||
parent
|
||||
.spawn(Node {
|
||||
width: Val::Percent(100.0),
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
column_gap: Val::Px(6.0),
|
||||
..default()
|
||||
})
|
||||
.with_children(|row| {
|
||||
row.spawn((
|
||||
Text::new(label),
|
||||
TextFont {
|
||||
font_size: LABEL_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
Node {
|
||||
width: Val::Px(128.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
row.spawn((
|
||||
Text::new("—"),
|
||||
TextFont {
|
||||
font_size: VALUE_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
Node {
|
||||
flex_grow: 1.0,
|
||||
..default()
|
||||
},
|
||||
ParamValue(value_key.to_string()),
|
||||
));
|
||||
// Wider button so the toggle label "ø" / "✓" reads clearly.
|
||||
parent_button(row, toggle, "ø", 34.0);
|
||||
});
|
||||
}
|
||||
|
||||
/// Marker on the value Text node so `refresh_control_panel_values` can find
|
||||
/// and update it. The `String` is the param field name (e.g. "seed", "twist").
|
||||
#[derive(Component)]
|
||||
pub(crate) struct ParamValue(pub(crate) String);
|
||||
|
||||
fn spawn_icon_button(parent: &mut ChildSpawnerCommands, label: &str, marker: ParamButton) {
|
||||
parent_button(parent, marker, label, 26.0);
|
||||
}
|
||||
|
||||
fn parent_button(
|
||||
parent: &mut ChildSpawnerCommands,
|
||||
marker: ParamButton,
|
||||
label: &str,
|
||||
width: f32,
|
||||
) {
|
||||
parent
|
||||
.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Px(26.0),
|
||||
width: Val::Px(width),
|
||||
height: Val::Px(26.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
@@ -329,6 +605,7 @@ pub fn param_button_handler(
|
||||
continue;
|
||||
};
|
||||
match button {
|
||||
// ── Galaxy ────────────────────────────────────────────────────
|
||||
ParamButton::SeedDecr => {
|
||||
params.seed = params.seed.wrapping_sub(SEED_STEP);
|
||||
params.bump_generation();
|
||||
@@ -337,33 +614,6 @@ pub fn param_button_handler(
|
||||
params.seed = params.seed.wrapping_add(SEED_STEP);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::CountDecr => {
|
||||
params.count = params.count.saturating_sub(COUNT_STEP).max(COUNT_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::CountIncr => {
|
||||
params.count = (params.count + COUNT_STEP).min(COUNT_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::ArmsDecr => {
|
||||
params.arms = params.arms.saturating_sub(1).max(ARMS_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::ArmsIncr => {
|
||||
params.arms = params.arms.saturating_add(1).min(ARMS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::VerticalArmsDecr => {
|
||||
params.vertical_arms = params.vertical_arms.saturating_sub(1);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::VerticalArmsIncr => {
|
||||
params.vertical_arms = params
|
||||
.vertical_arms
|
||||
.saturating_add(1)
|
||||
.min(VERTICAL_ARMS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::SizeDecr => {
|
||||
params.size = (params.size - SIZE_STEP).max(SIZE_MIN);
|
||||
params.bump_generation();
|
||||
@@ -372,20 +622,189 @@ pub fn param_button_handler(
|
||||
params.size = (params.size + SIZE_STEP).min(SIZE_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::TwistDecr => {
|
||||
params.twist = (params.twist - TWIST_STEP).max(TWIST_MIN);
|
||||
// ── Core ──────────────────────────────────────────────────────
|
||||
ParamButton::CoreCountDecr => {
|
||||
params.core.count = params
|
||||
.core
|
||||
.count
|
||||
.saturating_sub(CORE_COUNT_STEP)
|
||||
.max(CORE_COUNT_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::TwistIncr => {
|
||||
params.twist = (params.twist + TWIST_STEP).min(TWIST_MAX);
|
||||
ParamButton::CoreCountIncr => {
|
||||
params.core.count = (params.core.count + CORE_COUNT_STEP).min(CORE_COUNT_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::VerticalTwistDecr => {
|
||||
params.vertical_twist = (params.vertical_twist - VERTICAL_TWIST_STEP).max(VERTICAL_TWIST_MIN);
|
||||
ParamButton::CoreRadiusDecr => {
|
||||
params.core.radius = (params.core.radius - CORE_RADIUS_STEP).max(CORE_RADIUS_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::VerticalTwistIncr => {
|
||||
params.vertical_twist = (params.vertical_twist + VERTICAL_TWIST_STEP).min(VERTICAL_TWIST_MAX);
|
||||
ParamButton::CoreRadiusIncr => {
|
||||
params.core.radius = (params.core.radius + CORE_RADIUS_STEP).min(CORE_RADIUS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
// ── Disk ──────────────────────────────────────────────────────
|
||||
ParamButton::DiskCountDecr => {
|
||||
params.disk.count = params
|
||||
.disk
|
||||
.count
|
||||
.saturating_sub(DISK_COUNT_STEP)
|
||||
.max(DISK_COUNT_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::DiskCountIncr => {
|
||||
params.disk.count = (params.disk.count + DISK_COUNT_STEP).min(DISK_COUNT_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::DiskArmsDecr => {
|
||||
params.disk.arms = params.disk.arms.saturating_sub(1).max(DISK_ARMS_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::DiskArmsIncr => {
|
||||
params.disk.arms = params.disk.arms.saturating_add(1).min(DISK_ARMS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::DiskTwistDecr => {
|
||||
params.disk.twist = (params.disk.twist - DISK_TWIST_STEP).max(DISK_TWIST_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::DiskTwistIncr => {
|
||||
params.disk.twist = (params.disk.twist + DISK_TWIST_STEP).min(DISK_TWIST_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
// ── Top Beam (+Y) ─────────────────────────────────────────────
|
||||
ParamButton::BeamTopEnabled => {
|
||||
params.beam_top.enabled = !params.beam_top.enabled;
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopThicknessDecr => {
|
||||
params.beam_top.thickness =
|
||||
(params.beam_top.thickness - BEAM_THICKNESS_STEP).max(BEAM_THICKNESS_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopThicknessIncr => {
|
||||
params.beam_top.thickness =
|
||||
(params.beam_top.thickness + BEAM_THICKNESS_STEP).min(BEAM_THICKNESS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopLengthDecr => {
|
||||
params.beam_top.length =
|
||||
(params.beam_top.length - BEAM_LENGTH_STEP).max(BEAM_LENGTH_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopLengthIncr => {
|
||||
params.beam_top.length =
|
||||
(params.beam_top.length + BEAM_LENGTH_STEP).min(BEAM_LENGTH_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopDensityDecr => {
|
||||
params.beam_top.count = params
|
||||
.beam_top
|
||||
.count
|
||||
.saturating_sub(BEAM_COUNT_STEP)
|
||||
.max(BEAM_COUNT_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopDensityIncr => {
|
||||
params.beam_top.count =
|
||||
(params.beam_top.count + BEAM_COUNT_STEP).min(BEAM_COUNT_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopTaperDecr => {
|
||||
params.beam_top.taper =
|
||||
(params.beam_top.taper - BEAM_TAPER_STEP).max(BEAM_TAPER_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopTaperIncr => {
|
||||
params.beam_top.taper =
|
||||
(params.beam_top.taper + BEAM_TAPER_STEP).min(BEAM_TAPER_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopGravityDecr => {
|
||||
params.beam_top.gravity =
|
||||
(params.beam_top.gravity - BEAM_GRAVITY_STEP).max(BEAM_GRAVITY_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamTopGravityIncr => {
|
||||
params.beam_top.gravity =
|
||||
(params.beam_top.gravity + BEAM_GRAVITY_STEP).min(BEAM_GRAVITY_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
// ── Bottom Beam (-Y) ──────────────────────────────────────────
|
||||
ParamButton::BeamBottomEnabled => {
|
||||
params.beam_bottom.enabled = !params.beam_bottom.enabled;
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomThicknessDecr => {
|
||||
params.beam_bottom.thickness =
|
||||
(params.beam_bottom.thickness - BEAM_THICKNESS_STEP).max(BEAM_THICKNESS_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomThicknessIncr => {
|
||||
params.beam_bottom.thickness =
|
||||
(params.beam_bottom.thickness + BEAM_THICKNESS_STEP).min(BEAM_THICKNESS_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomLengthDecr => {
|
||||
params.beam_bottom.length =
|
||||
(params.beam_bottom.length - BEAM_LENGTH_STEP).max(BEAM_LENGTH_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomLengthIncr => {
|
||||
params.beam_bottom.length =
|
||||
(params.beam_bottom.length + BEAM_LENGTH_STEP).min(BEAM_LENGTH_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomDensityDecr => {
|
||||
params.beam_bottom.count = params
|
||||
.beam_bottom
|
||||
.count
|
||||
.saturating_sub(BEAM_COUNT_STEP)
|
||||
.max(BEAM_COUNT_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomDensityIncr => {
|
||||
params.beam_bottom.count =
|
||||
(params.beam_bottom.count + BEAM_COUNT_STEP).min(BEAM_COUNT_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomTaperDecr => {
|
||||
params.beam_bottom.taper =
|
||||
(params.beam_bottom.taper - BEAM_TAPER_STEP).max(BEAM_TAPER_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomTaperIncr => {
|
||||
params.beam_bottom.taper =
|
||||
(params.beam_bottom.taper + BEAM_TAPER_STEP).min(BEAM_TAPER_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomGravityDecr => {
|
||||
params.beam_bottom.gravity =
|
||||
(params.beam_bottom.gravity - BEAM_GRAVITY_STEP).max(BEAM_GRAVITY_MIN);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::BeamBottomGravityIncr => {
|
||||
params.beam_bottom.gravity =
|
||||
(params.beam_bottom.gravity + BEAM_GRAVITY_STEP).min(BEAM_GRAVITY_MAX);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::RandomSettings => {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
params.seed = rng.gen();
|
||||
params.size = rng.gen_range(SIZE_MIN..=SIZE_MAX);
|
||||
// Round size to step.
|
||||
params.size = (params.size / SIZE_STEP).round() * SIZE_STEP;
|
||||
params.core.count = rng.gen_range(CORE_COUNT_MIN..=CORE_COUNT_MAX);
|
||||
params.core.radius = rng.gen_range(CORE_RADIUS_MIN..=CORE_RADIUS_MAX);
|
||||
params.core.radius = (params.core.radius / CORE_RADIUS_STEP).round() * CORE_RADIUS_STEP;
|
||||
params.disk.count = rng.gen_range(DISK_COUNT_MIN..=DISK_COUNT_MAX);
|
||||
params.disk.count = (params.disk.count / DISK_COUNT_STEP) * DISK_COUNT_STEP;
|
||||
params.disk.arms = rng.gen_range(DISK_ARMS_MIN..=DISK_ARMS_MAX);
|
||||
params.disk.twist = rng.gen_range(DISK_TWIST_MIN..=DISK_TWIST_MAX);
|
||||
params.disk.twist = (params.disk.twist / DISK_TWIST_STEP).round() * DISK_TWIST_STEP;
|
||||
randomize_beam(&mut params.beam_top, &mut rng);
|
||||
randomize_beam(&mut params.beam_bottom, &mut rng);
|
||||
params.bump_generation();
|
||||
}
|
||||
ParamButton::Regenerate => params.reseed_and_bump(),
|
||||
@@ -416,20 +835,40 @@ pub fn reset_view_button_handler(
|
||||
|
||||
/// Refresh the displayed parameter values every frame so the UI stays in sync.
|
||||
/// Bevy UI has no native data binding, so we manually patch the `Text` children
|
||||
/// — cheap (≤14 nodes) and avoids re-spawning the whole panel.
|
||||
/// — cheap (≤25 nodes) and avoids re-spawning the whole panel.
|
||||
pub fn refresh_control_panel_values(
|
||||
params: Res<GalaxyParams>,
|
||||
mut values: Query<(&ParamValue, &mut Text)>,
|
||||
) {
|
||||
for (marker, mut text) in &mut values {
|
||||
let new = match marker.0.as_str() {
|
||||
// Galaxy
|
||||
"seed" => format!("{}", params.seed),
|
||||
"count" => format!("{}", params.count),
|
||||
"arms" => format!("{}", params.arms),
|
||||
"varms" => format!("{}", params.vertical_arms),
|
||||
"size" => format!("{:.0}", params.size),
|
||||
"twist" => format!("{:.1}", params.twist),
|
||||
"vtwist" => format!("{:.1}", params.vertical_twist),
|
||||
// Core
|
||||
"core_count" => format!("{}", params.core.count),
|
||||
"core_radius" => format!("{:.0}", params.core.radius),
|
||||
// Disk
|
||||
"disk_count" => format!("{}", params.disk.count),
|
||||
"disk_arms" => format!("{}", params.disk.arms),
|
||||
"disk_twist" => format!("{:.1}", params.disk.twist),
|
||||
// Top beam
|
||||
"beam_top_enabled" => format!("{}", if params.beam_top.enabled { "On" } else { "Off" }),
|
||||
"beam_top_thickness" => format!("{:.0}", params.beam_top.thickness),
|
||||
"beam_top_length" => format!("{:.0}", params.beam_top.length),
|
||||
"beam_top_density" => format!("{}", params.beam_top.count),
|
||||
"beam_top_taper" => format!("{:.1}", params.beam_top.taper),
|
||||
"beam_top_gravity" => format!("{:.1}", params.beam_top.gravity),
|
||||
// Bottom beam
|
||||
"beam_bottom_enabled" => format!(
|
||||
"{}",
|
||||
if params.beam_bottom.enabled { "On" } else { "Off" }
|
||||
),
|
||||
"beam_bottom_thickness" => format!("{:.0}", params.beam_bottom.thickness),
|
||||
"beam_bottom_length" => format!("{:.0}", params.beam_bottom.length),
|
||||
"beam_bottom_density" => format!("{}", params.beam_bottom.count),
|
||||
"beam_bottom_taper" => format!("{:.1}", params.beam_bottom.taper),
|
||||
"beam_bottom_gravity" => format!("{:.1}", params.beam_bottom.gravity),
|
||||
_ => continue,
|
||||
};
|
||||
if text.0 != new {
|
||||
@@ -443,3 +882,60 @@ pub fn refresh_control_panel_values(
|
||||
// `setup_galaxy_ui` runs on OnEnter(Galaxy) via the plugin in `mod.rs`.
|
||||
// Despawning happens through the shared `GalaxySpawned` marker in the
|
||||
// plugin's OnExit handler — no separate UI cleanup needed.
|
||||
|
||||
/// Randomize all fields of a [`BeamParams`] to random valid values within
|
||||
/// their respective bounds, rounded to the UI step.
|
||||
fn randomize_beam(beam: &mut BeamParams, rng: &mut impl rand::Rng) {
|
||||
beam.enabled = rng.gen();
|
||||
beam.count = rng.gen_range(BEAM_COUNT_MIN..=BEAM_COUNT_MAX);
|
||||
beam.count = (beam.count / BEAM_COUNT_STEP) * BEAM_COUNT_STEP;
|
||||
beam.thickness = rng.gen_range(BEAM_THICKNESS_MIN..=BEAM_THICKNESS_MAX);
|
||||
beam.thickness = (beam.thickness / BEAM_THICKNESS_STEP).round() * BEAM_THICKNESS_STEP;
|
||||
beam.length = rng.gen_range(BEAM_LENGTH_MIN..=BEAM_LENGTH_MAX);
|
||||
beam.length = (beam.length / BEAM_LENGTH_STEP).round() * BEAM_LENGTH_STEP;
|
||||
beam.taper = rng.gen_range(BEAM_TAPER_MIN..=BEAM_TAPER_MAX);
|
||||
beam.taper = (beam.taper / BEAM_TAPER_STEP).round() * BEAM_TAPER_STEP;
|
||||
beam.gravity = rng.gen_range(BEAM_GRAVITY_MIN..=BEAM_GRAVITY_MAX);
|
||||
beam.gravity = (beam.gravity / BEAM_GRAVITY_STEP).round() * BEAM_GRAVITY_STEP;
|
||||
}
|
||||
|
||||
/// Route mouse-wheel events to the control panel's scroll container when the
|
||||
/// cursor is over the panel. Bevy 0.16 does not wire `MouseWheel` to
|
||||
/// `ScrollPosition` automatically — this system bridges the gap.
|
||||
pub fn scroll_control_panel(
|
||||
mut scroll_events: EventReader<MouseWheel>,
|
||||
primary_window: Query<&Window, With<PrimaryWindow>>,
|
||||
panel_nodes: Query<(&ComputedNode, &GlobalTransform), With<GalaxyControlPanel>>,
|
||||
mut scroll_content: Query<&mut ScrollPosition, With<GalaxyScrollContent>>,
|
||||
) {
|
||||
let Ok(window) = primary_window.single() else {
|
||||
scroll_events.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(cursor_pos) = window.cursor_position() else {
|
||||
scroll_events.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if cursor is within any control panel node's rect.
|
||||
let over_panel = panel_nodes.iter().any(|(node, transform)| {
|
||||
let pos = transform.translation().truncate();
|
||||
let size = node.size();
|
||||
let min = pos - size * 0.5;
|
||||
let max = pos + size * 0.5;
|
||||
cursor_pos.x >= min.x && cursor_pos.x <= max.x && cursor_pos.y >= min.y && cursor_pos.y <= max.y
|
||||
});
|
||||
|
||||
if !over_panel {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(mut scroll_pos) = scroll_content.single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for event in scroll_events.read() {
|
||||
scroll_pos.offset_y -= event.y * 20.0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user