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)
158 lines
4.4 KiB
Rust
158 lines
4.4 KiB
Rust
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);
|
|
}
|
|
}
|