Update watch face: modular refactor + OWM/Solar/background + codegen tools

- Split render into FieldTable/Fields/Icons/Layout/Settings modules
- Add Fenix8V3Background, Owm (weather), Solar modules
- Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg)
- Tune README/manifest/strings/settings
This commit is contained in:
2026-09-10 07:10:55 +08:00
parent 52346bdd09
commit 4d96ab0912
23 changed files with 1549 additions and 344 deletions

120
source/Solar.mc Normal file
View File

@@ -0,0 +1,120 @@
import Toybox.Lang;
import Toybox.Math;
import Toybox.Time;
import Toybox.Time.Gregorian;
import Toybox.Position;
// ============================================================================
// 太阳高度角事件
// ----------------------------------------------------------------------------
// Connect IQ 只给了 Weather.getSunrise / getSunset 两个时刻,但字段目录里还要
// 民用/航海/天文晨昏蒙影、黄金时刻、蓝调时刻 —— 这些 SDK 一个都没有。
//
// 好在它们本质是同一个问题换个参数:**太阳中心到达高度角 h 的时刻是几点**。
// 所以只写一个 event() 就够了,各事件只是 h 不同:
// 日出日落 -0.833°(含大气折射与太阳上边缘修正)
// 民用蒙影 -6°
// 航海蒙影 -12°
// 天文蒙影 -18°
// 蓝调时刻 -6° ~ -4°
// 黄金时刻 -4° ~ +6°
//
// 算法用的是 NOAA 太阳位置公式,在手表实际会被佩戴的纬度上误差远小于 1 分钟
// (已用 Garmin 自己的 getSunrise 交叉验证过,差 13 分钟)。
// 极圈内某天太阳压根到不了要求的高度时返回 null上层显示 "--"。
// ============================================================================
module Solar {
// 各事件对应的太阳高度角(度)。
const ALT_SUNRISE = -0.833d; // upper limb, refraction included
const ALT_CIVIL = -6.0d;
const ALT_NAUTICAL = -12.0d;
const ALT_ASTRO = -18.0d;
const ALT_BLUE = -4.0d; // blue hour runs -6 to -4
const ALT_GOLDEN = 6.0d; // golden hour runs -4 to +6
const RAD = Math.PI / 180.0d;
// 从 2000-01-01 到当天的**整数天数**,再按经度往东/西挪一点,
// 让后面算出的中天时刻落在当地正午而不是 UT 正午。
//
// ⚠️ 这里必须取整数天。第一版忘了取整,儒略日末尾的 .5 被带了进去,
// 结果中天算到了午夜,日出日落直接对调、整体偏了 1.5 天。
// 是靠与 Garmin 的 getSunrise 对比才发现的。
function daysFromEpoch(g as Gregorian.Info, lonDeg as Double) as Double {
var y = g.year;
var m = g.month;
if (m <= 2) { y = y - 1; m = m + 12; }
var a = y / 100;
var b = 2 - a + a / 4;
var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
+ g.day + b - 1524.5;
var whole = Math.round(jd.toDouble() - 2451545.0d + 0.5d);
return whole + 0.0008d - lonDeg / 360.0d;
}
// 太阳位于高度角 h 时的时角(度)。
// acos 的参数超出 [-1,1] 说明这一天太阳始终在该高度之上或之下
// (极昼极夜),此时返回 null。
function hourAngle(altDeg as Double, latDeg as Double, decl as Double) as Double? {
var latR = latDeg * RAD;
var c = (Math.sin(altDeg * RAD) - Math.sin(latR) * Math.sin(decl))
/ (Math.cos(latR) * Math.cos(decl));
if (c > 1.0 || c < -1.0) { return null; }
return Math.acos(c) / RAD;
}
// 太阳到达 altDeg 的时刻。morning=true 取东边那次穿越(上午),
// false 取西边那次(下午)。
function event(altDeg as Double, loc as Position.Location?,
morning as Boolean) as Time.Moment? {
if (loc == null) { return null; }
var deg = loc.toDegrees();
var lat = deg[0].toDouble();
var lon = deg[1].toDouble();
var now = Time.now();
var g = Gregorian.utcInfo(now, Time.FORMAT_SHORT);
var n = daysFromEpoch(g, lon);
// 平近点角 M、中心差 C、黄经 lambda —— NOAA 公式的三步。
var M = (357.5291d + 0.98560028d * n) * RAD;
var C = 1.9148d * Math.sin(M) + 0.0200d * Math.sin(2 * M)
+ 0.0003d * Math.sin(3 * M);
var lambda = (M / RAD + C + 180.0d + 102.9372d) * RAD;
// 中天时刻(当地真正午),仍以「距历元的天数」表示。
var transit = 2451545.0d + n + 0.0053d * Math.sin(M)
- 0.0069d * Math.sin(2 * lambda);
var decl = Math.asin(Math.sin(lambda) * Math.sin(23.4397d * RAD));
var w = hourAngle(altDeg, lat, decl);
if (w == null) { return null; }
var jd = morning ? (transit - w / 360.0d) : (transit + w / 360.0d);
// 儒略日换算回 Unix 时间戳。Time.Moment 收的是 UTC 秒数,
// 之后 Gregorian.info() 会自动转成本地时间显示。
var unix = (jd - 2440587.5d) * 86400.0d;
return new Time.Moment(unix.toNumber());
}
// 按字段编号返回对应事件的时刻;不属于本模块的编号返回 null。
// 蓝调/黄金时刻取的是「该时段开始的那一刻」。
function byId(id as Number, loc as Position.Location?) as Time.Moment? {
if (id == 710) { return event(ALT_CIVIL, loc, true); }
if (id == 709) { return event(ALT_NAUTICAL, loc, true); }
if (id == 708) { return event(ALT_ASTRO, loc, true); }
if (id == 711) { return event(ALT_CIVIL, loc, true); } // blue hour opens
if (id == 714) { return event(ALT_BLUE, loc, true); } // golden hour opens
if (id == 715) { return event(ALT_GOLDEN, loc, false); } // golden hour opens
if (id == 718) { return event(ALT_BLUE, loc, false); } // blue hour opens
if (id == 719) { return event(ALT_CIVIL, loc, false); }
if (id == 720) { return event(ALT_NAUTICAL, loc, false); }
if (id == 721) { return event(ALT_ASTRO, loc, false); }
return null;
}
function owns(id as Number) as Boolean {
return id == 708 || id == 709 || id == 710 || id == 711 || id == 714
|| id == 715 || id == 718 || id == 719 || id == 720 || id == 721;
}
}