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

View File

@@ -1,20 +1,24 @@
#!/usr/bin/env python3
"""One table -> the field id list, both languages, and the settings XML.
"""一张表 → 字段 id 表、两种语言的文案、以及设置项 XML
Field ids follow the numbering at https://watchface.io/docs/datafields so the
two catalogues line up, but only the fields Connect IQ can actually serve from
the watch are here. Anything needing a web service (OpenWeatherMap,
StormGlass), a companion phone integration (the week-to-date and 7/28-day
per-sport aggregates), or another installed app (CGM, hydration, quotes) is
deliberately absent -- see README section 8 for the full reasoning.
字段编号沿用 https://watchface.io/docs/datafields,方便两边对照;但这里只收录
Connect IQ 在手表本机能算出来的字段。
需要联网服务的OpenWeatherMap 的部分、StormGlass、需要手机侧与 Garmin
Connect 集成的(按运动分类的周/月累计)、需要用户装了别的 App 的(CGM
补水、行情),都不在这里 —— 具体原因见 README 第 7 节。
python3 tools/gen_fields.py
Writes:
source/FieldTable.mc ids + short label lookup
resources/settings/settings.xml the four field pickers
resources/strings/fields.xml English names and labels
resources-chn/strings/fields.xml Chinese names and labels
产出:
source/FieldTable.mc 字段 id 表 + 下标反查
resources/settings/settings.xml 四个数据位的选择器
resources/strings/fields.xml 英文的字段名与表盘标签
resources-chn/strings/fields.xml 中文的字段名与表盘标签
加字段的流程:在下面的 FIELDS 表里加一行 → 跑一遍本脚本 →
到 source/Fields.mc 对应分类的函数里补一个分支。设置项、两种语言的文案、
表盘上的标签都会自动跟上,不会漏。
"""
import os
@@ -40,6 +44,8 @@ FIELDS = [
(868, "MonthNum", "Month (6)", "月份数字", "MON", ""),
(858, "Meridiem", "AM/PM", "上午/下午", "", ""),
(859, "WeekIso", "Week Number (ISO)", "周数 (ISO)", "WEEK", ""),
(850, "Seconds", "Seconds", "", "SEC", ""),
(869, "Seconds0", "Seconds (09)", "秒 (前导零)", "SEC", ""),
(873, "WeekCal", "Week Number", "周数", "WEEK", ""),
# ---- activity -------------------------------------------------------
@@ -58,6 +64,15 @@ FIELDS = [
(6, "MetersUp", "Distance Climbed", "爬升高度", "ASC", "爬升"),
(9, "PushDist", "Push Distance", "推行距离", "PUSH", "推行"),
(10, "Pushes", "Pushes", "推行次数", "PUSH", "推行"),
(219, "StepsWeek", "Steps (Week to Date)", "本周步数", "WEEK", "周步数"),
(220, "Steps7d", "Steps (Last 7 Days)", "近 7 天步数", "7D", "7天"),
(211, "CalWeek", "Calories (Week to Date)", "本周卡路里", "WEEK", "周千卡"),
(212, "Cal7d", "Calories (Last 7 Days)", "近 7 天卡路里", "7D", "7天"),
(213, "DistWeek", "Distance (Week to Date)", "本周距离", "WEEK", "周距离"),
(214, "Dist7d", "Distance (Last 7 Days)", "近 7 天距离", "7D", "7天"),
(203, "FloorsWeek", "Floors (Week to Date)", "本周楼层", "WEEK", "周楼层"),
(204, "Floors7d", "Floors (Last 7 Days)", "近 7 天楼层", "7D", "7天"),
(200, "ActiveMin7d", "Active Minutes (7 Days)", "近 7 天活动分钟", "7D", "7天"),
# ---- body -----------------------------------------------------------
(618, "HeartRate", "Heart Rate", "心率", "HR", "心率"),
@@ -94,6 +109,16 @@ FIELDS = [
(731, "UntilSun", "Time Until Sun Event", "距太阳事件", "SUN", "太阳"),
(706, "MoonAge", "Moon Age", "月龄", "MOON", "月龄"),
(732, "MoonLit", "Moon Illumination", "月球照明", "MOON", "月相"),
(710, "CivilAm", "Civil Twilight Start", "民用晨光始", "CIVIL", "民用晨"),
(709, "NauticalAm", "Nautical Twilight Start", "航海晨光始", "NAUT", "航海晨"),
(708, "AstroAm", "Astronomical Twilight Start", "天文晨光始", "ASTR", "天文晨"),
(711, "BlueAm", "Blue Hour (AM)", "蓝调时刻 (晨)", "BLUE", "蓝调晨"),
(714, "GoldenAm", "Golden Hour (AM)", "黄金时刻 (晨)", "GOLD", "黄金晨"),
(715, "GoldenPm", "Golden Hour (PM)", "黄金时刻 (昏)", "GOLD", "黄金昏"),
(718, "BluePm", "Blue Hour (PM)", "蓝调时刻 (昏)", "BLUE", "蓝调昏"),
(719, "CivilPm", "Civil Twilight End", "民用昏影终", "CIVIL", "民用昏"),
(720, "NauticalPm", "Nautical Twilight End", "航海昏影终", "NAUT", "航海昏"),
(721, "AstroPm", "Astronomical Twilight End", "天文昏影终", "ASTR", "天文昏"),
# ---- weather --------------------------------------------------------
(306, "Temp", "Temperature", "气温", "TEMP", "气温"),
@@ -107,6 +132,38 @@ FIELDS = [
(307, "WindBearing", "Wind Bearing", "风向角", "WIND", "风向"),
(310, "WindDir", "Wind Direction", "风向", "WIND", "风向"),
(309, "WeatherTime", "Weather Update Time", "天气更新时间", "WX", "更新"),
(312, "PressureSea", "Pressure (Sea Level)", "海平面气压", "hPa", "气压"),
(313, "Uvi", "UV Index", "紫外线指数", "UVI", "紫外线"),
(314, "Visibility", "Visibility", "能见度", "VIS", "能见度"),
(315, "DewPoint", "Dew Point", "露点", "DEW", "露点"),
# ---- OpenWeatherMap -------------------------------------------------
# Fetched by the background service; see source/Owm.mc.
(500, "OwmTemp", "OWM Temperature", "OWM 气温", "TEMP", "气温"),
(515, "OwmMinMax", "OWM Max/Min Temp", "OWM 最高/最低", "HI/LO", "高低温"),
(503, "OwmFeels", "OWM Feels Like", "OWM 体感温度", "FEEL", "体感"),
(504, "OwmMax", "OWM Temp Max", "OWM 最高温", "HIGH", "最高"),
(505, "OwmMin", "OWM Temp Min", "OWM 最低温", "LOW", "最低"),
(507, "OwmDescShort", "OWM Description", "OWM 天气", "WX", "天气"),
(502, "OwmDescLong", "OWM Description (Long)", "OWM 天气详述", "WX", "天气"),
(501, "OwmHumidity", "OWM Humidity", "OWM 湿度", "HUM", "湿度"),
(506, "OwmPressure", "OWM Pressure", "OWM 气压", "hPa", "气压"),
(509, "OwmVisibility", "OWM Visibility", "OWM 能见度", "VIS", "能见度"),
(510, "OwmWind", "OWM Wind Speed", "OWM 风速", "WIND", "风速"),
(511, "OwmWindDeg", "OWM Wind Degrees", "OWM 风向角", "WIND", "风向"),
(518, "OwmWindDir", "OWM Wind Direction", "OWM 风向", "WIND", "风向"),
(512, "OwmGust", "OWM Wind Gust", "OWM 阵风", "GUST", "阵风"),
(513, "OwmRain", "OWM Rain 1h", "OWM 1h 降雨", "RAIN", "降雨"),
(532, "OwmSnow", "OWM Snow 1h", "OWM 1h 降雪", "SNOW", "降雪"),
(536, "OwmClouds", "OWM Cloud Cover", "OWM 云量", "CLD", "云量"),
(537, "OwmPrecip", "OWM Precipitation %", "OWM 降水概率", "RAIN", "降水"),
(530, "OwmDew", "OWM Dew Point", "OWM 露点", "DEW", "露点"),
(534, "OwmTomorrow", "OWM Temp Tomorrow", "OWM 明日气温", "TMRW", "明日"),
(522, "OwmMinMaxTmrw", "OWM Min/Max Tomorrow", "OWM 明日高低温", "TMRW", "明日"),
(535, "OwmDayAfter", "OWM Temp Day After", "OWM 后日气温", "D+2", "后日"),
(523, "OwmMinMaxD2", "OWM Min/Max Day After", "OWM 后日高低温", "D+2", "后日"),
(516, "OwmCity", "OWM City Name", "OWM 城市", "CITY", "城市"),
(517, "OwmUpdated", "OWM Update Time", "OWM 更新时间", "OWM", "更新"),
# ---- custom ---------------------------------------------------------
(703, "Custom1", "Custom Text 1", "自定义文字 1", "", ""),
@@ -141,6 +198,10 @@ OTHER_SETTINGS = """
<settingConfig type="numeric" min="-720" max="840"/>
</setting>
<setting propertyKey="@Properties.OwmKey" title="@Strings.SettingOwmKey">
<settingConfig type="alphaNumeric" maxLength="40"/>
</setting>
<setting propertyKey="@Properties.Custom1" title="@Strings.SettingCustom1">
<settingConfig type="alphaNumeric" maxLength="12"/>
</setting>
@@ -184,11 +245,15 @@ def main():
assert len(ids) == len(set(ids)), "duplicate field id"
# ---- FieldTable.mc ----
L = ["// AUTO-GENERATED by tools/gen_fields.py -- do not edit by hand.",
"// Ids follow https://watchface.io/docs/datafields; only the fields a",
"// Connect IQ watch face can source on-device are listed.",
L = ["// ⚠️ 本文件由 tools/gen_fields.py 自动生成,请勿手改。",
"// 要加字段:在生成器的 FIELDS 表里加一行,跑一遍生成器,",
"// 再到 source/Fields.mc 对应分类的函数里补一个分支。",
"//",
"// 字段编号沿用 https://watchface.io/docs/datafields只收录 Connect IQ",
"// 表盘在本机能算出来的那些。",
"import Toybox.Lang;", "", "module FieldTable {", "",
" // Field ids, in the same order as the Labels resource string.",
" // 全部字段 id顺序与 FieldLabels 资源串(逗号分隔的标签表)严格一致 ——",
" // Fields.label() 就是靠这个下标去取对应标签的。",
" const IDS = ["]
row = " "
for i, fid in enumerate(ids):
@@ -198,6 +263,7 @@ def main():
row = " "
row += piece
L += [row, " ];", "",
" // 由字段 id 反查它在 IDS 里的下标;找不到返回 0对应「关闭」",
" function indexOf(id as Number) as Number {",
" for (var i = 0; i < IDS.size(); i++) {",
" if (IDS[i] == id) { return i; }",

View File

@@ -1,21 +1,24 @@
#!/usr/bin/env python3
"""Rasterise the design's icon paths into tintable bitmap resources.
"""把设计稿的图标路径光栅化成可染色的位图资源。
The sunrise and weather icons in design/*.svg are traced outlines whose finest
features are two or three design pixels across. Redrawing them from circles
and lines works at 454 px and falls apart at 280 px, where those features land
under one device pixel. Rendering the real paths once per screen size and
blitting them with drawBitmap2(:tintColor) keeps them exact everywhere.
design/*.svg 里的日出和天气图标是描边出来的轮廓,最细的特征只有 23 个设计
像素宽。用圆和线段去重画它们在 454px 上还行,到 280px 就彻底糊掉 —— 那个尺寸
下这些细节不足一个物理像素。
所以改成:把真实路径按每种屏宽各渲染一次,运行时用
drawBitmap2(:tintColor) 染上主题色。这样每台设备拿到的都是为它的像素网格
专门渲染的图。
python3 tools/gen_icons.py
Writes resources-icons-<W>/drawables/{sunrise,sunset,weather}.png plus the
matching drawables.xml. White pixels, alpha coverage; the theme's accent
colour is applied at draw time via :tintColor.
产出 resources-icons-<屏宽>/drawables/{sunrise,sunset,weather}.png 以及配套的
drawables.xml;另外产出 resources-launcher-<尺寸>/ 下各机型要求尺寸的启动图标。
MIP devices have no alpha blending, so for those widths the coverage mask is
thresholded to hard edges rather than left antialiased -- a soft edge there
would be quantised to something muddier than a clean one.
⚠️ 两个要点:
1. drawables.xml 里必须写 packingFormat="png"。否则资源编译器会把图片压成
调色板格式,而 :tintColor 拒绝调色板化的源,运行时直接抛异常。
2. MIP 屏没有 alpha 混合,所以那几个尺寸的覆盖率蒙版会被阈值化成硬边 ——
软边在 64 色调色板下反而更脏。
"""
import os
import re

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""Render preview-themes.svg and preview.html by replaying the face's draw calls.
"""重放表盘自己的绘制逻辑,产出 preview-themes.svg preview.html
Colours are parsed straight out of source/Themes.mc and the geometry constants
are the same literals as source/Fenix8V3View.mc, so the preview cannot drift
from the firmware. Text is drawn with a web font rather than Garmin's Roboto
Condensed / Bionic, so judge geometry and colour here, not glyph shapes.
颜色直接解析自 source/Themes.mc几何常量与 source/Fenix8V3View.mc /
source/Layout.mc 保持同一套字面值,所以预览不会和固件漂移。
⚠️ 文字用的是网页字体,真机是 Garmin 内置的 Roboto / Bionic字形会有差异。
**几何和配色可信,字形不可信** —— 判断字形要看模拟器截图。
python3 tools/gen_preview_svg.py
"""

View File

@@ -1,28 +1,27 @@
#!/usr/bin/env python3
"""Generate source/Themes.mc from the design kit in design/.
"""从 design/ 的设计稿生成 source/Themes.mc
Ground truth is the seven watchface-NN-*.svg files (produced by design/rebuild.py
from the original raster comps). Every colour written to Themes.mc is measured
from those files -- nothing here is hand-picked.
事实来源是七个 watchface-NN-*.svg原作者用 design/rebuild.py 从位图逆向出来的)。
写进 Themes.mc 的每一个颜色都是从这些文件里**量出来的**,没有一个是手挑的。
python3 tools/gen_themes.py
Extracted per theme:
每套主题提取出:
ringTop[21] lit colour of each top tick, index 0 = 6.5 deg .. 20 = 86.5 deg
ringBottom[21] lit colour of each bottom tick, index 0 = 6.0 deg .. 20 = 66.0 deg
tickOff colour of an unlit tick
anchorDot the four dots between the arcs
bandFill/Text date band background / foreground
colon the two colon blocks
hours[11] vertical gradient ramp of the hour digits, top -> bottom
minutes[11] ditto for the minute digits
accent icons and DST/STEP labels
textPrimary complication and bottom-row values
ringTop[21] 顶弧每根刻度点亮时的颜色,下标 0 = 6.5°,20 = 86.5°
ringBottom[21] 底弧每根刻度点亮时的颜色,下标 0 = 6.0°,20 = 66.0°
tickOff 未点亮刻度的颜色
anchorDot 四段弧之间的锚点圆点
bandFill/Text 日期带的底色 / 文字色
colon 两块冒号
hours[11] 小时数字的竖向渐变,从上到下
minutes[11] 分钟数字的竖向渐变
accent 图标与 DST/STEP 标签
textPrimary 各数据位与底部行的数值
Only 14 of the 21 bottom ticks are lit in any comp, so indices 14..20 of
ringBottom are extrapolated (least-squares fit over the last 8 measured ticks).
Those ticks only appear once a metric passes ~67%.
⚠️ 已知近似:设计稿里底弧最多只点亮 14/21 根,所以 ringBottom 的第 1521 项
无法实测,由最后 8 根做最小二乘线性外推得到。只有当指标超过约 67% 时才会
显示这几根。
"""
import os
import re
@@ -160,11 +159,20 @@ def fmt_list(colours, indent):
def main():
themes = [read_theme(t) for t in ORDER]
L = []
L.append("// AUTO-GENERATED by tools/gen_themes.py from design/watchface-*.svg.")
L.append("// Do not edit by hand -- change the design kit and re-run the generator.")
L.append("// ⚠️ 本文件由 tools/gen_themes.py design/watchface-*.svg 自动生成,请勿手改。")
L.append("// 要改配色就改设计稿里的 SVG然后重新跑一遍生成器。")
L.append("//")
L.append("// ringTop/ringBottom lit colour per tick, index 0 nearest 12 / 6 o'clock")
L.append("// hours/minutes 11-stop vertical gradient of the time digits, top -> bottom")
L.append("// 七套主题的全部颜色数据。各数组的下标就是主题序号0=Ember … 6=Kelp")
L.append("//")
L.append("// RINGTOP/RINGBOTTOM 每根刻度点亮时的颜色,各 21 项。")
L.append("// 下标 0 是最靠近 12 点 / 6 点的那根。")
L.append("// 这是从设计稿里**逐根取样**的实测值,不是插值近似。")
L.append("// HOURS/MINUTES 时间数字的竖向渐变,各 11 个停止点,从上到下。")
L.append("// TICKOFF 未点亮刻度的颜色")
L.append("// ANCHORDOT 四个锚点圆点")
L.append("// BANDFILL/BANDTEXT 日期带的底色 / 文字色")
L.append("// ACCENT 图标与底部标签")
L.append("// TEXTPRIMARY 各项数值")
L.append("import Toybox.Lang;")
L.append("")
L.append("module Themes {")