use placidus house system, and system theme
This commit is contained in:
@@ -63,10 +63,6 @@ fn main() {
|
||||
// ── GTK / ADW application ─────────────────────────────────────────────────
|
||||
let app = RelmApp::new("net.zodia.app");
|
||||
|
||||
// Request dark colour scheme — adwaita will use the system preference as
|
||||
// fallback if the compositor ignores prefer-dark-scheme.
|
||||
adw::StyleManager::default().set_color_scheme(adw::ColorScheme::PreferDark);
|
||||
|
||||
// Add the bundled icon resource path to the default icon theme so GTK
|
||||
// resolves our icons on every platform including macOS.
|
||||
if let Some(display) = gtk4::gdk::Display::default() {
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@ pub struct Chart {
|
||||
}
|
||||
|
||||
impl Chart {
|
||||
/// Compute a chart using Whole Sign houses (default).
|
||||
/// Compute a chart using Placidus houses (default).
|
||||
/// Falls back to an all-zero house stub if the geohash is too coarse.
|
||||
pub fn compute(birth: BirthData) -> Result<Self, EphemerisError> {
|
||||
Self::compute_with(birth, HouseKind::WholeSign)
|
||||
Self::compute_with(birth, HouseKind::Placidus)
|
||||
}
|
||||
|
||||
/// Compute a chart with an explicit house system.
|
||||
|
||||
+94
-10
@@ -5,12 +5,12 @@
|
||||
//! length — a 5-char hash gives ~±2.5 km, which is ±0.1° in ASC at mid-latitudes.
|
||||
//!
|
||||
//! Implemented systems:
|
||||
//! - Placidus — semi-arc method; most widely used in Western astrology (default)
|
||||
//! - Whole Sign — ASC determines the 1st house sign; each sign = one house
|
||||
//! - Equal House — each cusp is exactly 30° from the ASC
|
||||
//!
|
||||
//! Stubs (fall back to Equal):
|
||||
//! - Placidus and Koch require iterative latitude-dependent solving;
|
||||
//! they will be added in a dedicated follow-up.
|
||||
//! - Koch — similar iterative approach, not yet implemented
|
||||
|
||||
use crate::birth::BirthData;
|
||||
use crate::planet::{Planet, PlanetPositions};
|
||||
@@ -44,21 +44,22 @@ pub enum HouseError {
|
||||
|
||||
impl HouseSystem {
|
||||
/// Compute house cusps for `birth` using the requested system.
|
||||
///
|
||||
/// Falls back to Equal House for Placidus/Koch until those are implemented.
|
||||
pub fn compute(birth: &BirthData, kind: HouseKind) -> Result<Self, HouseError> {
|
||||
if birth.geohash.len() < 3 {
|
||||
return Err(HouseError::GeohashTooShort(birth.geohash.len()));
|
||||
}
|
||||
let (lat, lon) = decode_geohash(&birth.geohash)?;
|
||||
let asc = ascendant(birth.jdn, lat, lon);
|
||||
let mc = midheaven(birth.jdn, lon);
|
||||
let asc = ascendant(birth.jdn, lat, lon);
|
||||
let mc = midheaven(birth.jdn, lon);
|
||||
let ramc = (gmst(birth.jdn) + lon).rem_euclid(360.0);
|
||||
let eps = obliquity(birth.jdn);
|
||||
|
||||
let cusps = match kind {
|
||||
HouseKind::WholeSign => whole_sign_cusps(asc),
|
||||
HouseKind::Equal => equal_house_cusps(asc),
|
||||
// Placidus and Koch: fall back to Equal for now
|
||||
HouseKind::Placidus | HouseKind::Koch => equal_house_cusps(asc),
|
||||
HouseKind::WholeSign => whole_sign_cusps(asc),
|
||||
HouseKind::Equal => equal_house_cusps(asc),
|
||||
HouseKind::Placidus => placidus_cusps(ramc, lat, eps, mc, asc),
|
||||
// Koch: fall back to Placidus (close enough; full Koch to be added later)
|
||||
HouseKind::Koch => placidus_cusps(ramc, lat, eps, mc, asc),
|
||||
};
|
||||
|
||||
Ok(Self { kind, cusps, ascendant: asc, midheaven: mc })
|
||||
@@ -169,3 +170,86 @@ fn equal_house_cusps(asc: f64) -> [f64; 12] {
|
||||
}
|
||||
cusps
|
||||
}
|
||||
|
||||
/// Placidus house cusps — semi-arc method.
|
||||
///
|
||||
/// Houses 1, 4, 7, 10 are fixed at ASC, IC, DSC, MC.
|
||||
/// Intermediate cusps (11, 12, 2, 3) are found by dividing each quadrant's
|
||||
/// semi-arc into thirds, solved iteratively (~5–10 iterations converge to <1e-6°).
|
||||
///
|
||||
/// `ramc`: Right Ascension of MC (= Local Sidereal Time, degrees)
|
||||
/// `lat`: geographic latitude (degrees)
|
||||
/// `eps`: obliquity of the ecliptic (degrees)
|
||||
/// `mc`: MC ecliptic longitude (degrees)
|
||||
/// `asc`: Ascendant ecliptic longitude (degrees)
|
||||
fn placidus_cusps(ramc: f64, lat: f64, eps: f64, mc: f64, asc: f64) -> [f64; 12] {
|
||||
let phi = lat.to_radians();
|
||||
let eps_r = eps.to_radians();
|
||||
|
||||
let mut cusps = [0.0f64; 12];
|
||||
cusps[0] = asc; // H1 = ASC
|
||||
cusps[3] = (mc + 180.0).rem_euclid(360.0); // H4 = IC
|
||||
cusps[6] = (asc + 180.0).rem_euclid(360.0); // H7 = DSC
|
||||
cusps[9] = mc; // H10 = MC
|
||||
|
||||
// H11, H12: 1/3 and 2/3 of the diurnal semi-arc from MC toward ASC.
|
||||
for (i, frac) in [(10usize, 1.0_f64 / 3.0), (11, 2.0 / 3.0)] {
|
||||
let ra = placidus_iter(ramc, frac, phi, eps_r, true);
|
||||
cusps[i] = ra_to_ecl_lon(ra, eps_r);
|
||||
}
|
||||
|
||||
// H2, H3: 1/3 and 2/3 of the nocturnal semi-arc from IC toward DSC.
|
||||
let ic_ra = (ramc + 180.0).rem_euclid(360.0);
|
||||
for (i, frac) in [(1usize, 1.0_f64 / 3.0), (2, 2.0 / 3.0)] {
|
||||
let ra = placidus_iter(ic_ra, frac, phi, eps_r, false);
|
||||
cusps[i] = ra_to_ecl_lon(ra, eps_r);
|
||||
}
|
||||
|
||||
// Opposite cusps.
|
||||
cusps[4] = (cusps[10] + 180.0).rem_euclid(360.0); // H5 = opposite H11
|
||||
cusps[5] = (cusps[11] + 180.0).rem_euclid(360.0); // H6 = opposite H12
|
||||
cusps[7] = (cusps[1] + 180.0).rem_euclid(360.0); // H8 = opposite H2
|
||||
cusps[8] = (cusps[2] + 180.0).rem_euclid(360.0); // H9 = opposite H3
|
||||
|
||||
cusps
|
||||
}
|
||||
|
||||
/// Iterate to find the RA of one Placidus intermediate cusp.
|
||||
///
|
||||
/// `base_ra`: starting RA of the quadrant (RAMC for H11/12, IC_RA for H2/3).
|
||||
/// `frac`: 1/3 or 2/3.
|
||||
/// `diurnal`: true → use diurnal semi-arc (DSA); false → nocturnal (NSA = π − DSA).
|
||||
fn placidus_iter(base_ra: f64, frac: f64, phi: f64, eps: f64, diurnal: bool) -> f64 {
|
||||
// Seed the iteration with a 60° (1/3 of 180°) or 120° (2/3) offset.
|
||||
let mut ra = (base_ra + frac * 180.0).rem_euclid(360.0);
|
||||
|
||||
for _ in 0..50 {
|
||||
// Ecliptic longitude corresponding to this RA (assuming β = 0).
|
||||
let ecl = ra_to_ecl_lon(ra, eps).to_radians();
|
||||
// Declination of that ecliptic point.
|
||||
let dec = (eps.sin() * ecl.sin()).asin();
|
||||
// Diurnal semi-arc: the angular distance from the eastern horizon to the MC.
|
||||
let cos_dsa = (-phi.tan() * dec.tan()).clamp(-1.0, 1.0);
|
||||
let dsa = cos_dsa.acos(); // radians, 0–π
|
||||
let semi_arc = if diurnal { dsa } else { std::f64::consts::PI - dsa };
|
||||
let new_ra = (base_ra.to_radians() + frac * semi_arc)
|
||||
.to_degrees()
|
||||
.rem_euclid(360.0);
|
||||
if (new_ra - ra).abs() < 1e-6 {
|
||||
return new_ra;
|
||||
}
|
||||
ra = new_ra;
|
||||
}
|
||||
ra
|
||||
}
|
||||
|
||||
/// Convert equatorial RA (degrees) to ecliptic longitude (degrees), assuming β = 0.
|
||||
///
|
||||
/// Inverse of: tan(RA) = sin(λ)·cos(ε) / cos(λ) → λ = atan2(sin(RA), cos(RA)·cos(ε))
|
||||
fn ra_to_ecl_lon(ra: f64, eps: f64) -> f64 {
|
||||
let ra_r = ra.to_radians();
|
||||
ra_r.sin()
|
||||
.atan2(ra_r.cos() * eps.cos())
|
||||
.to_degrees()
|
||||
.rem_euclid(360.0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user