Add galaxy parameter UI, star selection, and movement/physics scaffolding
Galaxy creation scene (Bevy 0.16): - Split into module folder: params.rs, mod.rs (generation + spawn), ui.rs, selection.rs - GalaxyParams resource (seed, count, arms, vertical_arms, size, twist, vertical_twist) with generation counter for change detection - Control panel: 7 +/- slider rows + Regenerate button, no native Bevy slider widget so uses label + icon buttons - Info panel: live-refreshes on selection change via despawn_related::<Children> - Click-to-select: screen-space picking (project each star to viewport, closest within 18px threshold wins); selected star lerps scale to 2.2x - Generation faithfully ports the docs TS reference including vertical arms - Regeneration system: despawns GalaxyScene root only, preserves UI panels Camera: - Camera2d -> Camera3d + OrbitCamera (left-drag yaw/pitch, scroll zoom, pitch/distance clamped to avoid gimbal) - Orbit drag suppressed when cursor over UI New plugins (scaffolding): - movement/: Velocity, MaxSpeed, TurnRate, Drag, MoveTarget, Player components + click-to-move (no player spawned yet) - physics/: pure-data geometry (ray_vs_sphere, overlaps, separate, segment_vs_sphere) with 7 passing unit tests; no systems wired yet - star_map/: plugin skeleton gated on AppState::InGame Shared: - ui/util.rs: cursor_over_ui helper for UI-hover detection - docs: ARCH-9 decision record (custom movement & collision, no physics engine)
This commit is contained in:
157
apps/game/src/gameplay/physics/geometry.rs
Normal file
157
apps/game/src/gameplay/physics/geometry.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// Ray-sphere intersection. Returns the smallest positive `t` where
|
||||
/// `ray_origin + ray_direction * t` lies on the sphere's surface, or `None`.
|
||||
///
|
||||
/// `ray_direction` should be normalized.
|
||||
pub fn ray_vs_sphere(
|
||||
ray_origin: Vec3,
|
||||
ray_direction: Vec3,
|
||||
sphere_center: Vec3,
|
||||
sphere_radius: f32,
|
||||
) -> Option<f32> {
|
||||
let to_sphere = sphere_center - ray_origin;
|
||||
let proj = to_sphere.dot(ray_direction);
|
||||
if proj < 0.0 {
|
||||
return None; // sphere is behind the ray origin
|
||||
}
|
||||
let closest = ray_origin + ray_direction * proj;
|
||||
let dist_sq = (closest - sphere_center).length_squared();
|
||||
let r_sq = sphere_radius * sphere_radius;
|
||||
if dist_sq > r_sq {
|
||||
return None; // ray misses the sphere
|
||||
}
|
||||
let offset = (r_sq - dist_sq).sqrt();
|
||||
Some(proj - offset)
|
||||
}
|
||||
|
||||
/// Check if two spheres overlap. Use length-squared for performance (no sqrt).
|
||||
pub fn overlaps(
|
||||
a_center: Vec3,
|
||||
a_radius: f32,
|
||||
b_center: Vec3,
|
||||
b_radius: f32,
|
||||
) -> bool {
|
||||
let combined = a_radius + b_radius;
|
||||
(a_center - b_center).length_squared() < combined * combined
|
||||
}
|
||||
|
||||
/// Compute the vector to push `a` out of `b` so they no longer overlap.
|
||||
/// Returns `None` if they don't overlap, or if their centers coincide (ambiguous direction).
|
||||
pub fn separate(
|
||||
a_center: Vec3,
|
||||
a_radius: f32,
|
||||
b_center: Vec3,
|
||||
b_radius: f32,
|
||||
) -> Option<Vec3> {
|
||||
let delta = a_center - b_center;
|
||||
let combined = a_radius + b_radius;
|
||||
let dist = delta.length();
|
||||
if dist >= combined || dist == 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(delta / dist * (combined - dist))
|
||||
}
|
||||
|
||||
/// Segment (finite line from `seg_start` to `seg_end`) vs sphere intersection.
|
||||
/// Returns `t` in `[0, segment_length]` measured from `seg_start`, or `None`.
|
||||
///
|
||||
/// Useful for projectile hit detection (treat projectile last-frame → current-frame
|
||||
/// positions as the segment to avoid tunneling at high speeds).
|
||||
pub fn segment_vs_sphere(
|
||||
seg_start: Vec3,
|
||||
seg_end: Vec3,
|
||||
sphere_center: Vec3,
|
||||
sphere_radius: f32,
|
||||
) -> Option<f32> {
|
||||
let seg = seg_end - seg_start;
|
||||
let seg_len = seg.length();
|
||||
if seg_len == 0.0 {
|
||||
return if (seg_start - sphere_center).length_squared() < sphere_radius * sphere_radius {
|
||||
Some(0.0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
let dir = seg / seg_len;
|
||||
let t = ray_vs_sphere(seg_start, dir, sphere_center, sphere_radius)?;
|
||||
if t > seg_len {
|
||||
return None;
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ray_hits_sphere_directly() {
|
||||
let t = ray_vs_sphere(
|
||||
Vec3::new(0.0, 0.0, -5.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::ZERO,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(t, Some(4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_misses_sphere() {
|
||||
let t = ray_vs_sphere(
|
||||
Vec3::new(10.0, 0.0, -5.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::ZERO,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(t, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_with_sphere_behind_returns_none() {
|
||||
let t = ray_vs_sphere(
|
||||
Vec3::new(0.0, 0.0, 5.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::ZERO,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(t, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_spheres_detected() {
|
||||
assert!(overlaps(Vec3::ZERO, 1.0, Vec3::new(1.5, 0.0, 0.0), 1.0));
|
||||
assert!(!overlaps(Vec3::ZERO, 1.0, Vec3::new(3.0, 0.0, 0.0), 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separate_pushes_a_out_of_b() {
|
||||
let sep = separate(Vec3::new(0.5, 0.0, 0.0), 1.0, Vec3::ZERO, 1.0);
|
||||
// Combined radius 2.0, current distance 0.5, so push by 1.5 along +X.
|
||||
assert_eq!(sep, Some(Vec3::new(1.5, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_hits_sphere() {
|
||||
let t = segment_vs_sphere(
|
||||
Vec3::new(0.0, 0.0, -5.0),
|
||||
Vec3::new(0.0, 0.0, 5.0),
|
||||
Vec3::ZERO,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(t, Some(4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_misses_when_sphere_off_line() {
|
||||
let t = segment_vs_sphere(
|
||||
Vec3::new(0.0, 10.0, -5.0),
|
||||
Vec3::new(0.0, 10.0, 5.0),
|
||||
Vec3::ZERO,
|
||||
1.0,
|
||||
);
|
||||
assert_eq!(t, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user