diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3197ce7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,29 @@
+# 更新记录
+
+## 1.1.0
+
+**机型覆盖 17 → 55 款**
+
+新增 38 款圆屏、Connect IQ ≥ 4.2 的机型,包括 fenix 7 Pro 全系、Venu 2/3S、
+Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、FR255S/265S/570/955 等。
+机型列表与资源路径改由 `tools/gen_devices.py` 扫描本机 SDK 生成,不再手写。
+
+新增两档图标尺寸(218px、360px)。218px 上设计稿的天气图标只有 14×14,
+太阳与云之间的暗缝不足一个像素,会糊成一块 —— 该尺寸改用专画的简化标记。
+
+**其它**
+
+- 补 LICENSE(MIT,仅覆盖代码;`design/` 下的设计稿版权归第三方,见文件内说明)
+- `UserProfile.getProfile()` 加异常保护
+- 版本号 1.0.0 → 1.1.0
+
+## 1.0.0
+
+按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。
+
+- 156 项可选数据字段,编号对齐 watchface.io 目录
+- 七套主题,配色逐根刻度实测自设计稿
+- 常亮(AOD)模式,含防烧屏位移
+- 简体中文 / 英文双语,表盘文字亦本地化
+- OpenWeatherMap 接入(用户自备 API key,后台定时拉取)
+- 系统 Complication 接入(日历、训练状态、完赛预测、第三方 App)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..bb7ae9f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+MIT License
+
+Copyright (c) 2026 ericwyuan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---------------------------------------------------------------------------
+注意:本许可只覆盖**本仓库的代码**。
+
+design/ 目录下的设计稿(watchface-*.svg、watchface-tokens.json、rebuild.py)
+来自第三方 watchface-kit,版权归其原作者所有,不在本许可范围内。
+表盘的视觉呈现是对那套设计的 1:1 还原 —— 若要发布到 Connect IQ Store 或以
+其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。
diff --git a/README.md b/README.md
index 565dce8..6671182 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Fenix 8 V3 — Garmin 数据表盘
-按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED(Fenix 8 / Epix 2 / Venu 3 等)与圆屏 MIP(Fenix 7 / Enduro 3 / FR255 等)共 17 款设备。
+按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED 与圆屏 MIP 共 **55 款**设备(Fenix 7/8 全系含 Pro、Epix 2、Enduro 3、Venu 2/3、FR 系列、Approach S50/S70、Descent、D2、Instinct 3 AMOLED 等)。
- **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,`minSdkVersion="4.2.0"`)
- **语言**:Monkey C;界面文案含英文与简体中文
@@ -24,6 +24,7 @@ fenix8v3-watchface/
├── tools/
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
+│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
├── source/
@@ -49,10 +50,14 @@ fenix8v3-watchface/
重新生成全部派生文件:
```bash
-python3 tools/gen_themes.py && python3 tools/gen_fields.py \
+python3 tools/gen_devices.py \
+ && python3 tools/gen_themes.py && python3 tools/gen_fields.py \
&& python3 tools/gen_icons.py && python3 tools/gen_preview_svg.py
```
+`gen_devices.py` 要先跑 —— 它决定了需要哪些图标尺寸,`gen_icons.py` 的
+`WIDTHS` / `LAUNCHER` 两张表必须跟它对齐,少一档就会有机型编译不过。
+
三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
---
@@ -283,7 +288,16 @@ AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有
- 时间与日期压暗到 55%,其余数据位全部隐藏
- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
-只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型(Fenix 7 / Enduro 3 等)不受影响,照常全量绘制。
+只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型不受影响,照常全量绘制。
+
+实测(fenix847mm,取表盘内区):
+
+| | 点亮像素数 | 平均亮度(功耗代理) |
+|---|---|---|
+| 普通模式 | 27.6% | 14.1% |
+| 常亮模式 | 10.4% | **2.9%** |
+
+两个口径差别很大:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,和实际功耗对不上。曾试过连进度环也砍掉,平均亮度只从 3.01% 降到 2.91% —— 0.1 个百分点换掉一个可见特征不划算,所以环保留。
模拟器可用 `Settings → Display Mode` 切换验证。
@@ -328,7 +342,9 @@ for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar
done
```
-屏幕分辨率跨度较大(fenix7s 240×240 至 fenix847mm / venu3 454×454),全部按 `s = width/500` 缩放。
+屏幕分辨率跨度较大(FR255S 218×218 至 454×454 共 8 档),全部按 `s = width/500` 缩放。
+
+机型列表和 `monkey.jungle` 由 `tools/gen_devices.py` 扫描本机已安装的 SDK 设备生成:筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 需要 4.2)。**注意结果取决于本机装了哪些设备**,跑之前先确认 SDK 设备装全了,脚本会打印找到的数量。
---
diff --git a/manifest.xml b/manifest.xml
index 800dd4c..deda23b 100644
--- a/manifest.xml
+++ b/manifest.xml
@@ -7,26 +7,64 @@
minSdkVersion="4.2.0"
name="@Strings.AppName"
type="watchface"
- version="1.0.0">
+ version="1.1.0">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/monkey.jungle b/monkey.jungle
index 500909f..3870674 100644
--- a/monkey.jungle
+++ b/monkey.jungle
@@ -2,31 +2,68 @@ project.manifest = manifest.xml
base.sourcePath = source
-# resources-chn holds the Simplified Chinese variant of every string; the
-# -chn suffix is what tells monkeyc which language it belongs to.
+# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。
base.resourcePath = resources;resources-chn
-# Two more resource trees per device, both generated by tools/gen_icons.py:
-# resources-icons- sunrise/sunset/weather rasterised from the
-# design paths for that exact pixel grid
-# resources-launcher- launcher icon at the size the device asks for
+# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:
+# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标
+# resources-launcher-<尺寸> 该机型要求尺寸的启动图标
+# 这一整段由 tools/gen_devices.py 生成,不要手改。
+approachs50.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
+approachs7042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+approachs7047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
+d2airx10.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
+d2mach1.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+d2mach2.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+descentg2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+descentmk343mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+descentmk351mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+epix2pro42mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fenix7pro.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fenix7pronowifi.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix7s.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
+fenix7spro.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
fenix7x.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+fenix7xpro.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+fenix7xpronowifi.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fenix8pro47mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
+fenixe.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+fr165.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
+fr165m.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
fr255.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fr255m.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
+fr255s.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
+fr255sm.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
fr265.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+fr265s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-60
+fr57042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
+fr57047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+fr955.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fr965.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+fr970.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
+instinct3amoled45mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+instinct3amoled50mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
+instinctcrossoveramoled.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-38
+marq2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+marq2aviator.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
+venu2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
+venu2plus.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
+venu2s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-61
venu3.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
+venu3s.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-70
+venu441mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
+venu445mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
+vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
base.excludeAnnotations = release
diff --git a/resources-icons-218/drawables/drawables.xml b/resources-icons-218/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-218/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-218/drawables/sunrise.png b/resources-icons-218/drawables/sunrise.png
new file mode 100644
index 0000000..e6f1ecf
Binary files /dev/null and b/resources-icons-218/drawables/sunrise.png differ
diff --git a/resources-icons-218/drawables/sunset.png b/resources-icons-218/drawables/sunset.png
new file mode 100644
index 0000000..5a8c238
Binary files /dev/null and b/resources-icons-218/drawables/sunset.png differ
diff --git a/resources-icons-218/drawables/weather.png b/resources-icons-218/drawables/weather.png
new file mode 100644
index 0000000..3ebfc13
Binary files /dev/null and b/resources-icons-218/drawables/weather.png differ
diff --git a/resources-icons-240/drawables/weather.png b/resources-icons-240/drawables/weather.png
index dfb6b84..5c1fac7 100644
Binary files a/resources-icons-240/drawables/weather.png and b/resources-icons-240/drawables/weather.png differ
diff --git a/resources-icons-360/drawables/drawables.xml b/resources-icons-360/drawables/drawables.xml
new file mode 100644
index 0000000..b1703a8
--- /dev/null
+++ b/resources-icons-360/drawables/drawables.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/resources-icons-360/drawables/sunrise.png b/resources-icons-360/drawables/sunrise.png
new file mode 100644
index 0000000..7fe584c
Binary files /dev/null and b/resources-icons-360/drawables/sunrise.png differ
diff --git a/resources-icons-360/drawables/sunset.png b/resources-icons-360/drawables/sunset.png
new file mode 100644
index 0000000..abdec91
Binary files /dev/null and b/resources-icons-360/drawables/sunset.png differ
diff --git a/resources-icons-360/drawables/weather.png b/resources-icons-360/drawables/weather.png
new file mode 100644
index 0000000..14904ce
Binary files /dev/null and b/resources-icons-360/drawables/weather.png differ
diff --git a/resources-launcher-38/drawables/drawables.xml b/resources-launcher-38/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-38/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-38/drawables/launcher_icon.png b/resources-launcher-38/drawables/launcher_icon.png
new file mode 100644
index 0000000..2afa717
Binary files /dev/null and b/resources-launcher-38/drawables/launcher_icon.png differ
diff --git a/resources-launcher-54/drawables/drawables.xml b/resources-launcher-54/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-54/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-54/drawables/launcher_icon.png b/resources-launcher-54/drawables/launcher_icon.png
new file mode 100644
index 0000000..c1e371b
Binary files /dev/null and b/resources-launcher-54/drawables/launcher_icon.png differ
diff --git a/resources-launcher-61/drawables/drawables.xml b/resources-launcher-61/drawables/drawables.xml
new file mode 100644
index 0000000..579343d
--- /dev/null
+++ b/resources-launcher-61/drawables/drawables.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/resources-launcher-61/drawables/launcher_icon.png b/resources-launcher-61/drawables/launcher_icon.png
new file mode 100644
index 0000000..03602b2
Binary files /dev/null and b/resources-launcher-61/drawables/launcher_icon.png differ
diff --git a/source/Fenix8V3View.mc b/source/Fenix8V3View.mc
index 4350119..eb041f2 100644
--- a/source/Fenix8V3View.mc
+++ b/source/Fenix8V3View.mc
@@ -281,6 +281,12 @@ class Fenix8V3View extends WatchUi.WatchFace {
// 低功耗画面。相比正常画面砍掉的东西:
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45%)
+ //
+ // 关于点亮量:实测普通模式的平均亮度(AMOLED 功耗的合理代理)是 14.1%,
+ // 常亮是 2.9%,约 1/5。曾经试过连进度环也砍掉,平均亮度只从 3.01% 降到
+ // 2.91% —— 0.1 个百分点换掉一个可见特征,不划算,所以环留着。
+ // (常按「点亮像素数」算会得到 10% 左右,但那个口径把每个抗锯齿边缘像素
+ // 都算成「亮」,跟实际功耗对不上。)
// - 四个数据位、电池(全部隐藏)
// 保留的时间和日期压暗到 55%。
//
diff --git a/source/Fields.mc b/source/Fields.mc
index b7a4079..62235ac 100644
--- a/source/Fields.mc
+++ b/source/Fields.mc
@@ -244,8 +244,18 @@ module Fields {
return null;
}
+ // 本项目已经被这个 API 家族坑过三次(Properties.getValue、
+ // deleteTemporalEvent、Complication.unit),一律按「声明了权限也可能抛」
+ // 来防。取不到就当作没有用户档案,相关字段显示 "--"。
function profile() as UserProfile.Profile? {
- if (!mGotProf) { mProf = UserProfile.getProfile(); mGotProf = true; }
+ if (!mGotProf) {
+ try {
+ mProf = UserProfile.getProfile();
+ } catch (e) {
+ mProf = null;
+ }
+ mGotProf = true;
+ }
return mProf;
}
diff --git a/tools/gen_devices.py b/tools/gen_devices.py
new file mode 100644
index 0000000..9d3d749
--- /dev/null
+++ b/tools/gen_devices.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+"""扫描本机安装的 Connect IQ 设备,重写 manifest 的机型列表和 monkey.jungle。
+
+筛选条件(三条都要满足):
+ 1. 支持 watchFace 类型
+ 2. Connect IQ >= 4.2 —— Toybox.Complications 是这个版本引入的,
+ 本表盘的日历/训练状态/第三方 App 字段依赖它
+ 3. 圆屏(deviceFamily 以 round- 开头)
+
+ python3 tools/gen_devices.py
+
+改写两处:
+ manifest.xml 整块
+ monkey.jungle 每台设备的 resourcePath(图标按屏宽、启动图标按各自要求的尺寸)
+
+⚠️ 结果取决于本机 SDK 里**装了哪些设备**。如果某台机器上少装了几款,
+跑一遍会把它们从 manifest 里删掉。所以脚本会打印找到的数量,
+提交前确认一下数字对不对(当前应为 55 款)。
+
+图标资源的尺寸集合由本脚本算出后交给 tools/gen_icons.py 使用,
+两者的 WIDTHS / LAUNCHER 必须一致,所以改完这里要跟着跑一遍 gen_icons.py。
+"""
+import json
+import os
+import re
+from collections import defaultdict
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.dirname(HERE)
+DEV = os.path.expanduser(
+ "~/Library/Application Support/Garmin/ConnectIQ/Devices")
+
+
+def eligible():
+ """返回 [(设备id, 屏宽, 启动图标尺寸, 是否支持alpha), ...],按 id 排序。"""
+ out = []
+ if not os.path.isdir(DEV):
+ raise SystemExit("找不到设备目录:%s" % DEV)
+ for d in sorted(os.listdir(DEV)):
+ p = os.path.join(DEV, d, "compiler.json")
+ if not os.path.exists(p):
+ continue
+ try:
+ c = json.load(open(p))
+ except Exception:
+ continue
+ types = {t["type"] for t in c.get("appTypes", []) if isinstance(t, dict)}
+ if "watchFace" not in types:
+ continue
+ if not str(c.get("deviceFamily", "")).startswith("round"):
+ continue
+ vers = [str(pn.get("connectIQVersion", "0"))
+ for pn in c.get("partNumbers", [])]
+ if not any(_ciq_ok(v) for v in vers):
+ continue
+ li = c.get("launcherIcon", {})
+ out.append((d, c["resolution"]["width"], li.get("width"),
+ bool(c.get("alphaBlendingSupport"))))
+ return out
+
+
+def _ciq_ok(v):
+ try:
+ a = [int(x) for x in v.split(".")[:2]]
+ return a[0] > 4 or (a[0] == 4 and a[1] >= 2)
+ except Exception:
+ return False
+
+
+def write_manifest(devs):
+ p = os.path.join(ROOT, "manifest.xml")
+ s = open(p).read()
+ body = "\n".join(' ' % d for d, _, _, _ in devs)
+ block = " \n%s\n " % body
+ s = re.sub(r" .*?", block, s, flags=re.S)
+ open(p, "w").write(s)
+ print("manifest.xml: 写入 %d 款机型" % len(devs))
+
+
+def write_jungle(devs):
+ L = ["project.manifest = manifest.xml", "",
+ "base.sourcePath = source", "",
+ "# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。",
+ "base.resourcePath = resources;resources-chn", "",
+ "# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:",
+ "# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标",
+ "# resources-launcher-<尺寸> 该机型要求尺寸的启动图标",
+ "# 这一整段由 tools/gen_devices.py 生成,不要手改。",
+ ""]
+ for d, w, li, _ in devs:
+ L.append("%s.resourcePath = $(base.resourcePath)"
+ ";resources-icons-%d;resources-launcher-%d" % (d, w, li))
+ L += ["", "base.excludeAnnotations = release"]
+ open(os.path.join(ROOT, "monkey.jungle"), "w").write("\n".join(L) + "\n")
+ print("monkey.jungle: 写入 %d 条资源路径" % len(devs))
+
+
+def main():
+ devs = eligible()
+ if not devs:
+ raise SystemExit("一款符合条件的机型都没找到,请检查 SDK 设备是否已安装")
+ widths = defaultdict(set)
+ for _, w, _, alpha in devs:
+ widths[w].add(alpha)
+ print("符合条件的机型:%d 款" % len(devs))
+ print("需要的图标屏宽:%s" % sorted(widths))
+ print("需要的启动图标尺寸:%s" % sorted({li for _, _, li, _ in devs}))
+ for w in sorted(widths):
+ if len(widths[w]) > 1:
+ print(" ⚠️ %dpx 同时存在支持/不支持 alpha 的机型,"
+ "图标按硬边生成" % w)
+ write_manifest(devs)
+ write_jungle(devs)
+ print("\n接着跑:python3 tools/gen_icons.py")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/gen_icons.py b/tools/gen_icons.py
index 97b8fd0..9e343eb 100644
--- a/tools/gen_icons.py
+++ b/tools/gen_icons.py
@@ -41,8 +41,11 @@ ICONS = {
"weather": ("weather-icon", 343.55, 89.5, False),
}
-# screen width -> whether the panel can blend alpha (MIP cannot)
-WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True}
+# 屏宽 -> 该尺寸下的屏幕能否做 alpha 混合(MIP 屏不能)。
+# 这张表和启动图标尺寸表都由 tools/gen_devices.py 扫描出的机型集合决定,
+# 改完设备列表要跟着更新这里 —— 少一个尺寸就会有机型编译不过。
+WIDTHS = {218: False, 240: False, 260: False, 280: False,
+ 360: True, 390: True, 416: True, 454: True}
SS = 8 # supersampling factor
@@ -118,8 +121,8 @@ def render(paths, size, cx, cy, blend):
return img
-# Launcher icon sizes the target devices ask for.
-LAUNCHER = [40, 56, 60, 65, 70]
+# 各机型要求的启动图标尺寸(Garmin 每款表都有自己的规定值,不能用缩放糊弄)。
+LAUNCHER = [38, 40, 54, 56, 60, 61, 65, 70]
def launcher(size):
@@ -160,6 +163,46 @@ def launcher(size):
return img.resize((size, size), Image.LANCZOS)
+def tiny_weather(size):
+ """给 16px 以下的屏幕专画的天气标记。
+
+ 设计稿的天气图标是「太阳被云挡住一半」,靠一道暗缝把两者分开。那道缝在
+ 500 画布上只有 1.7px,到 218px 屏(图标 14×14)连一个物理像素都占不到,
+ 光栅化出来就是一坨没有结构的橙块。
+
+ 所以这个尺寸不再走设计稿路径,改用按目标像素直接摆的简化标记:
+ 右上一个太阳圆盘,左下一朵云,中间强制留 1 个像素的空隙。
+ 形不如原图,但至少还认得出是「天气」。
+ """
+ from PIL import ImageDraw
+ ss = 8
+ n = size * ss
+ img = Image.new("L", (n, n), 0)
+ d = ImageDraw.Draw(img)
+ u = n / 14.0 # 以 14 格为基准摆放,任何尺寸都等比
+
+ def disc(cx, cy, r, fill):
+ d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=fill)
+
+ # 太阳 + 四道光芒(光芒画成小方块,比线段在低分辨率下更容易留下来)
+ disc(9.2 * u, 4.6 * u, 2.7 * u, 255)
+ for dx, dy in ((0, -4.4), (4.3, 0), (-3.2, -3.2), (3.2, -3.2)):
+ d.rectangle([9.2 * u + dx * u - 0.55 * u, 4.6 * u + dy * u - 0.55 * u,
+ 9.2 * u + dx * u + 0.55 * u, 4.6 * u + dy * u + 0.55 * u],
+ fill=255)
+ # 先用黑色把云的轮廓“撑大”一圈,留出与太阳之间的缝,再画回本体
+ for grow, fill in ((1.0 * u, 0), (0.0, 255)):
+ disc(5.0 * u, 8.6 * u, 3.3 * u + grow, fill)
+ disc(8.6 * u, 9.6 * u, 2.4 * u + grow, fill)
+ d.rectangle([1.7 * u - grow, 8.8 * u - grow, 11.0 * u + grow, 12.2 * u],
+ fill=fill)
+ small = img.resize((size, size), Image.LANCZOS)
+ small = small.point(lambda v: 255 if v >= 110 else 0)
+ out = Image.new("RGB", (size, size), (255, 255, 255)).convert("RGBA")
+ out.putalpha(small)
+ return out
+
+
def main():
svg = open(SRC).read()
for size in LAUNCHER:
@@ -182,7 +225,11 @@ def main():
if key not in cache:
p = subpaths(svg, pid)
cache[key] = flip_chevron(p) if flip else p
- img = render(cache[key], size, cx, cy, blend)
+ # 天气图标在极小尺寸下用专门的简化画法,日出/日落还撑得住。
+ if name == "weather" and size < 16:
+ img = tiny_weather(size)
+ else:
+ img = render(cache[key], size, cx, cy, blend)
img.save(os.path.join(outdir, "%s.png" % name))
names.append(name)
with open(os.path.join(outdir, "drawables.xml"), "w") as f: