机型覆盖 17 → 55 款,补 LICENSE 与 CHANGELOG
机型
新增 38 款圆屏 + CIQ ≥ 4.2 的机型:fenix 7 Pro 全系、Venu 2/3S、
Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、
FR255S/265S/570/955 等。其中 34 款的屏宽已有现成图标资源。
机型列表和 monkey.jungle 改由 tools/gen_devices.py 扫描本机 SDK 生成,
筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 要 4.2)。
55 条资源路径不再手写。
新增 218px 与 360px 两档图标。218px 上图标只有 14×14,设计稿里太阳与云
之间那道 1.7 设计像素的暗缝连一个物理像素都占不到,光栅化出来是一坨没有
结构的橙块 —— 该尺寸改用按目标像素直接摆的简化标记。
常亮模式:结论是不改
为了压所谓的 10% 点亮率,一度把进度环也砍掉了。换成平均亮度(AMOLED 功耗
的合理代理)重新量之后发现:普通模式 14.1%,常亮 2.9%,本来就只有 1/5;
砍掉环只从 3.01% 降到 2.91%。0.1 个百分点换掉一个可见特征不划算,环留着。
两种度量的差异和这个决定都写进了代码注释和 README。
其它
- LICENSE(MIT)。只覆盖代码 —— design/ 下的设计稿版权归第三方,
发布前需取得原作者授权或替换视觉,文件内已注明。
- UserProfile.getProfile() 加异常保护(这个 API 家族已经坑过三次)
- CHANGELOG.md,版本号 1.0.0 → 1.1.0
验证:55 款全部编译通过(分三批 18+18+19);.iq 上架包可正常导出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
118
tools/gen_devices.py
Normal file
118
tools/gen_devices.py
Normal file
@@ -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 <iq:products> 整块
|
||||
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(' <iq:product id="%s"/>' % d for d, _, _, _ in devs)
|
||||
block = " <iq:products>\n%s\n </iq:products>" % body
|
||||
s = re.sub(r" <iq:products>.*?</iq:products>", 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()
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user