fix: 12px 伪粗体文字自动加 1px 字距,消除相邻字粘连
- 12px 点阵字形间只有 1 列空隙,伪粗体右偏 1px 正好填满导致左右重叠;13/24px 空隙 2 列不受影响 - 基类新增 _track():12px 且加粗时自动加 k 字距,text/rtext/ctext/_mid 统一走它,宽版映射到 12px 的 F13/F16/FPACE 同样命中 - 新增 _tw_b() 取含粗体偏移与字距的实际占宽,替换环比/同比居中、金额前后缀、状态栏推进、BTC 涨跌幅、用量行进度条右端的手写宽度 - 夹具回归:小屏 F13/F16 部分逐像素不变,各行进度条右端仍齐平 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ EPD_HOST=epd400a44.local python3 main.py
|
|||||||
- **`_trend_chart`(当前隐藏)**:近30天三指标折线,用 `_styled_polyline()` 按 solid/dashed/dotted 区分、主指标(流水)红线,各指标按自身极值独立归一化。已保留但 `render()` 未调用,想恢复即在 C 前挂回。
|
- **`_trend_chart`(当前隐藏)**:近30天三指标折线,用 `_styled_polyline()` 按 solid/dashed/dotted 区分、主指标(流水)红线,各指标按自身极值独立归一化。已保留但 `render()` 未调用,想恢复即在 C 前挂回。
|
||||||
- **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类(sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog,识别不了回退 cloudy),再由 `_weather_icon()` 用 `_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。
|
- **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类(sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog,识别不了回退 cloudy),再由 `_weather_icon()` 用 `_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。
|
||||||
- **`pusher.py`** — 直连局域网墨水屏设备(喵喵固件,开源:https://gitee.com/gxp666111/miaomiao,`web/` 即设备自带管理页,可浏览器打开 `http://<EPD_HOST>/` 调试同一套接口)。`push_image()` 先调 `GET /images` 拿画廊已存图片名单,逐个 `POST /delete_image?name=`清空(单张删除失败不阻塞后续),再 `POST /upload`(`Content-Type: image/png`,body 为原始 PNG 字节,非 multipart)触发设备转换+刷屏。**清空画廊是刻意设计**:设备把每次 `/upload` 都追加存一份到画廊(`GET /images` 可见),不清理会越推越多、且设备当前 `current_mode` 为 slideshow 会轮播到旧图;先清后传使画廊恒为 1 张,等价于始终静态展示最新一张。**清理非核心**:拉取画廊列表本身失败也不阻塞后续 `/upload`(跳过清理,直接推)。`_request()` 对连接类异常(`urllib.error.URLError`,含 mDNS `.local` 域名解析瞬时失败)内置重试(`RETRY_TIMES=2`,间隔 `RETRY_DELAY=2s`)——mDNS 基于组播 UDP,偶发丢包很正常,单次失败不该让整轮推送直接跳过。注:历史上曾走 Zectrix 公网 API(`POST /devices/{id}/display/image` + 设备 MAC + `ZECTRIX_API_KEY`),现统一为局域网直连。
|
- **`pusher.py`** — 直连局域网墨水屏设备(喵喵固件,开源:https://gitee.com/gxp666111/miaomiao,`web/` 即设备自带管理页,可浏览器打开 `http://<EPD_HOST>/` 调试同一套接口)。`push_image()` 先调 `GET /images` 拿画廊已存图片名单,逐个 `POST /delete_image?name=`清空(单张删除失败不阻塞后续),再 `POST /upload`(`Content-Type: image/png`,body 为原始 PNG 字节,非 multipart)触发设备转换+刷屏。**清空画廊是刻意设计**:设备把每次 `/upload` 都追加存一份到画廊(`GET /images` 可见),不清理会越推越多、且设备当前 `current_mode` 为 slideshow 会轮播到旧图;先清后传使画廊恒为 1 张,等价于始终静态展示最新一张。**清理非核心**:拉取画廊列表本身失败也不阻塞后续 `/upload`(跳过清理,直接推)。`_request()` 对连接类异常(`urllib.error.URLError`,含 mDNS `.local` 域名解析瞬时失败)内置重试(`RETRY_TIMES=2`,间隔 `RETRY_DELAY=2s`)——mDNS 基于组播 UDP,偶发丢包很正常,单次失败不该让整轮推送直接跳过。注:历史上曾走 Zectrix 公网 API(`POST /devices/{id}/display/image` + 设备 MAC + `ZECTRIX_API_KEY`),现统一为局域网直连。
|
||||||
- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`,宽版置 0——百分比已是整数倍字号不需手动加字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(贴 x=790);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「上次更新 HH:MM」(与今日实时同款文案)或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+21 / 值 `_value` F24 粗+小号 ¥/万 y0+38 / 环比 y0+72 / 同比 y0+88(按墨迹框:标题↔名 ~9px——2026-09-14 整列下移 4px,原 ~5px 用户觉得挤;名↔值 ~9px、值↔环比 ~6px),竖规 y0+26~102,区块底 y0+104;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充、边框、百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。
|
- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`(小屏 F16 百分比的手动字距),宽版置 0——其 F16 映射到 12px、加粗后由 `_track()` 自动加 1px 字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(贴 x=790);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「上次更新 HH:MM」(与今日实时同款文案)或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+21 / 值 `_value` F24 粗+小号 ¥/万 y0+38 / 环比 y0+72 / 同比 y0+88(按墨迹框:标题↔名 ~9px——2026-09-14 整列下移 4px,原 ~5px 用户觉得挤;名↔值 ~9px、值↔环比 ~6px),竖规 y0+26~102,区块底 y0+104;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充、边框、百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。
|
||||||
- **`server.py`** — E1002 动态服务(stdlib `ThreadingHTTPServer`,无第三方依赖)。`build_data()` → `(data, 各路状态)`:日期按 `config.TZ_NAME` 时区现算;四路上游(天气/橘喵/BTC/设备遥测)经 `_cached(key, ttl, fetch)`——`STATE.last_good[key]` 未过期(`time - STATE.fetched_at[key] < ttl`)直接用;过期的**并行**提交到线程池 `_POOL`(`_refresh` 跑 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`,成功更新数据与时间戳),`build_data` 用 `concurrent.futures.wait` 整体最多等 **`FETCH_BUDGET`** 秒(默认 5,`.env` 可调),再由 `_settle` 结算:完成→`fetched(0.27s)`(带耗时)/失败沿用旧值 `stale`/从未成功占位 `none`;**未完成→ `slow→stale`(用上一次成功值)或 `slow→none`(占位)**,拉取线程继续跑完写入缓存供下次请求(stale-while-revalidate),`STATE.inflight[key]` 保证同一路进行中不重复实拉。响应上限 ≈ 预算 + 渲染,任一路上游卡死(socket 超时和风/橘喵 30s、BTC 8s)不再拖垮整图。**2026-09-14 前为串行实拉**(三路全拉约 1.1~2.6s,单路卡住即整体超时);改并行后三路全拉约 0.75s(取最慢一路)。日志每行末尾 `[weather:cache(12s) mao:fetched(0.06s) btc:slow→stale]` 可直接看命中/耗时/超预算情况,`/health` 有 `cache_age_s`/`ttl_s`/`fetch_budget_s`/`inflight`;用量取 `STATE.raw`(推送暂存)→ `data._usage(now, raw=…)`/`data._usage_gpt(now, raw=…)`,未推送时 Claude 全 `--`、ChatGPT 用 mock。端点:`GET IMAGE_PATH` → `render_png_bytes()`(`_RENDER_LOCK` 串行化,Pillow 渲染器非线程安全);`GET HTML_PATH` → `html_renderer.render_html()`;`GET HTML_PNG_PATH` → `html_shot.screenshot_html()`+`quantize6()`(`?raw=1` 跳过量化);`GET /health` JSON;`POST /push/usage|/push/usage_gpt`(body=原始快照 JSON,≤64KB,头 `X-Push-Token`==`PUSH_TOKEN`,为空拒绝);`GET /push/*` 查看暂存(同令牌)。所有响应 `Cache-Control: no-store`;每请求打一行日志(含 `CF-Connecting-IP`)。`PUSH_STATE_PATH` 非空时推送数据落盘(原子替换),重启恢复;图片永不落盘。实测:三路全实拉约 1.1~1.3s(天气 0.28s、橘喵 0.06s、OKX 0.7s 串行),全部命中缓存约 0.02s(渲染 20ms + 编码 14ms)。
|
- **`server.py`** — E1002 动态服务(stdlib `ThreadingHTTPServer`,无第三方依赖)。`build_data()` → `(data, 各路状态)`:日期按 `config.TZ_NAME` 时区现算;四路上游(天气/橘喵/BTC/设备遥测)经 `_cached(key, ttl, fetch)`——`STATE.last_good[key]` 未过期(`time - STATE.fetched_at[key] < ttl`)直接用;过期的**并行**提交到线程池 `_POOL`(`_refresh` 跑 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`,成功更新数据与时间戳),`build_data` 用 `concurrent.futures.wait` 整体最多等 **`FETCH_BUDGET`** 秒(默认 5,`.env` 可调),再由 `_settle` 结算:完成→`fetched(0.27s)`(带耗时)/失败沿用旧值 `stale`/从未成功占位 `none`;**未完成→ `slow→stale`(用上一次成功值)或 `slow→none`(占位)**,拉取线程继续跑完写入缓存供下次请求(stale-while-revalidate),`STATE.inflight[key]` 保证同一路进行中不重复实拉。响应上限 ≈ 预算 + 渲染,任一路上游卡死(socket 超时和风/橘喵 30s、BTC 8s)不再拖垮整图。**2026-09-14 前为串行实拉**(三路全拉约 1.1~2.6s,单路卡住即整体超时);改并行后三路全拉约 0.75s(取最慢一路)。日志每行末尾 `[weather:cache(12s) mao:fetched(0.06s) btc:slow→stale]` 可直接看命中/耗时/超预算情况,`/health` 有 `cache_age_s`/`ttl_s`/`fetch_budget_s`/`inflight`;用量取 `STATE.raw`(推送暂存)→ `data._usage(now, raw=…)`/`data._usage_gpt(now, raw=…)`,未推送时 Claude 全 `--`、ChatGPT 用 mock。端点:`GET IMAGE_PATH` → `render_png_bytes()`(`_RENDER_LOCK` 串行化,Pillow 渲染器非线程安全);`GET HTML_PATH` → `html_renderer.render_html()`;`GET HTML_PNG_PATH` → `html_shot.screenshot_html()`+`quantize6()`(`?raw=1` 跳过量化);`GET /health` JSON;`POST /push/usage|/push/usage_gpt`(body=原始快照 JSON,≤64KB,头 `X-Push-Token`==`PUSH_TOKEN`,为空拒绝);`GET /push/*` 查看暂存(同令牌)。所有响应 `Cache-Control: no-store`;每请求打一行日志(含 `CF-Connecting-IP`)。`PUSH_STATE_PATH` 非空时推送数据落盘(原子替换),重启恢复;图片永不落盘。实测:三路全实拉约 1.1~1.3s(天气 0.28s、橘喵 0.06s、OKX 0.7s 串行),全部命中缓存约 0.02s(渲染 20ms + 编码 14ms)。
|
||||||
- **`html_shot.py`** — 方案三:`screenshot_html(html)` 用无头 Chrome(`find_chrome()` 自动探测或 `CHROME_BIN`;Linux root 需 `CHROME_EXTRA_ARGS=--no-sandbox`)把 HTML 截成 800×480,临时目录用后即删,`_SHOT_LOCK` 串行化;**切勿传 `--user-data-dir` 指向全新目录**——新版 `--headless=new` 对空 profile 首启会卡死到超时(实测 A/B/D/E 全超时、只有不带它的 C 2 秒完成)。`quantize6(img)`:每通道二值化(阈值 `HTML_PNG_THRESHOLD` 默认 160,偏向保墨迹,细笔画浅灰边归黑而非丢白)→ Pillow 调色板 `quantize(dither=NONE)` 映射到 `SPECTRA6`,输出保证只含六色。服务端 `?raw=1` 返回未量化截图供对照。实测单次约 3.3s(Chrome 启动为主)。
|
- **`html_shot.py`** — 方案三:`screenshot_html(html)` 用无头 Chrome(`find_chrome()` 自动探测或 `CHROME_BIN`;Linux root 需 `CHROME_EXTRA_ARGS=--no-sandbox`)把 HTML 截成 800×480,临时目录用后即删,`_SHOT_LOCK` 串行化;**切勿传 `--user-data-dir` 指向全新目录**——新版 `--headless=new` 对空 profile 首启会卡死到超时(实测 A/B/D/E 全超时、只有不带它的 C 2 秒完成)。`quantize6(img)`:每通道二值化(阈值 `HTML_PNG_THRESHOLD` 默认 160,偏向保墨迹,细笔画浅灰边归黑而非丢白)→ Pillow 调色板 `quantize(dither=NONE)` 映射到 `SPECTRA6`,输出保证只含六色。服务端 `?raw=1` 返回未量化截图供对照。实测单次约 3.3s(Chrome 启动为主)。
|
||||||
- **`html_renderer.py`** — 方案二网页版:纯 HTML+CSS+内联 SVG、无 JS,数据服务端写死;字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底(平台截图机若无中文字体会掉字,能否加载取决于其出网能力);颜色只用六色纯值;配色规则与 PNG 版一致(用量/pace 三档、涨红跌黑、K 线涨绿跌红),版式同构。K 线标题右侧价格用 `<strong>`(`h3 b::before` 的方块装饰只给标题)。
|
- **`html_renderer.py`** — 方案二网页版:纯 HTML+CSS+内联 SVG、无 JS,数据服务端写死;字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底(平台截图机若无中文字体会掉字,能否加载取决于其出网能力);颜色只用六色纯值;配色规则与 PNG 版一致(用量/pace 三档、涨红跌黑、K 线涨绿跌红),版式同构。K 线标题右侧价格用 `<strong>`(`h3 b::before` 的方块装饰只给标题)。
|
||||||
@@ -76,7 +76,7 @@ EPD_HOST=epd400a44.local python3 main.py
|
|||||||
|
|
||||||
- **`DashboardRenderer(k=…)` 缩放系数**:所有组件(`_label`/`_cmp_line`/`_usage_row`/`_pace`/`tri` 等)的像素常量与伪粗体偏移都乘 `k`,字体按 `12*k/24*k/48*k` 创建;`k=1` 为 400×300 原版(改组件后务必用固定夹具比对 `k=1` 输出逐像素不变),E1002 版当前用 `k=1`(k=2 整体放大在 800×480 上字过大,已否决),k 参数保留供更高分辩率设备使用。字体属性名(`F12/F24/F48`)表达层级而非绝对像素。
|
- **`DashboardRenderer(k=…)` 缩放系数**:所有组件(`_label`/`_cmp_line`/`_usage_row`/`_pace`/`tri` 等)的像素常量与伪粗体偏移都乘 `k`,字体按 `12*k/24*k/48*k` 创建;`k=1` 为 400×300 原版(改组件后务必用固定夹具比对 `k=1` 输出逐像素不变),E1002 版当前用 `k=1`(k=2 整体放大在 800×480 上字过大,已否决),k 参数保留供更高分辩率设备使用。字体属性名(`F12/F24/F48`)表达层级而非绝对像素。
|
||||||
- 字体固定用 **Fusion Pixel 12px 点阵字体**(`fonts/`,OFL 协议),首选整数倍尺寸 **12/24/48**(`F12/F24/F48`)最锐利;用量区为拿到合适的字号层级另用了 **F13/F16**(1.08x/1.33x 非整数倍,笔画略不均,属有意取舍),靠 `d.fontmode = "1"` 关抗锯齿仍保持纯色不糊。跨字号竖直居中统一用 `_mid()`(按 `getbbox` 墨迹中心对齐)。
|
- 字体固定用 **Fusion Pixel 12px 点阵字体**(`fonts/`,OFL 协议),首选整数倍尺寸 **12/24/48**(`F12/F24/F48`)最锐利;用量区为拿到合适的字号层级另用了 **F13/F16**(1.08x/1.33x 非整数倍,笔画略不均,属有意取舍),靠 `d.fontmode = "1"` 关抗锯齿仍保持纯色不糊。跨字号竖直居中统一用 `_mid()`(按 `getbbox` 墨迹中心对齐)。
|
||||||
- 加粗用「伪粗体」:`_draw()` 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。
|
- 加粗用「伪粗体」:`_draw()` 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。**12px 加粗自动加 1px 字距**(`_track()`,2026-09-14):12px 字形之间只有 1 列空隙,伪粗体右偏 1px 正好填满、相邻字粘连(13/24px 空隙 2 列不受影响);`text/rtext/ctext/_mid` 内部都走它,宽版映射到 12px 的 F13/F16/FPACE 同样命中。手动推进 x 或居中时用 `_tw_b(s, ft, bold)` 取含粗体偏移与字距的实际占宽,别再手写 `_tw + bold`。
|
||||||
- 推送时 `DITHER = False`(硬阈值),纯黑白图最锐利,勿改成 `true`。
|
- 推送时 `DITHER = False`(硬阈值),纯黑白图最锐利,勿改成 `true`。
|
||||||
- 坐标、字号均为像素网格上的硬编码常量;调布局时注意各区分隔线 `hdash` 的 y 值与下方组件位置联动。
|
- 坐标、字号均为像素网格上的硬编码常量;调布局时注意各区分隔线 `hdash` 的 y 值与下方组件位置联动。
|
||||||
|
|
||||||
|
|||||||
32
renderer.py
32
renderer.py
@@ -75,14 +75,25 @@ class DashboardRenderer:
|
|||||||
self.d.text((cx + dx, y), ch, font=ft, fill=fill)
|
self.d.text((cx + dx, y), ch, font=ft, fill=fill)
|
||||||
cx += self.d.textlength(ch, font=ft) + track
|
cx += self.d.textlength(ch, font=ft) + track
|
||||||
|
|
||||||
|
def _track(self, ft, bold, track=0):
|
||||||
|
"""实际字距:显式 track 优先;否则「12px 且加粗」自动加 k——12px 点阵字形之间只有 1 列空隙,伪粗体右偏 1px 正好填满、
|
||||||
|
相邻字左右粘连(13/24px 空隙有 2 列所以正常),2026-09-14 用户要求拉开一点点。宽版 F13/F16/FPACE 都映射到 12px,同样命中。"""
|
||||||
|
if track or not bold:
|
||||||
|
return track
|
||||||
|
return self.k if getattr(ft, "size", 0) == 12 * self.k else 0
|
||||||
|
|
||||||
|
def _tw_b(self, s, ft, bold=0, track=0):
|
||||||
|
"""含伪粗体偏移与自动字距的实际占宽(手动推进 x / 居中时用它,别再手写 _tw + bold)。"""
|
||||||
|
return self._tw(s, ft, self._track(ft, bold, track)) + bold
|
||||||
|
|
||||||
def text(self, x, y, s, ft, fill=BLACK, bold=0):
|
def text(self, x, y, s, ft, fill=BLACK, bold=0):
|
||||||
self._draw(x, y, s, ft, fill, bold)
|
self._draw(x, y, s, ft, fill, bold, self._track(ft, bold))
|
||||||
|
|
||||||
def rtext(self, xr, y, s, ft, fill=BLACK, bold=0):
|
def rtext(self, xr, y, s, ft, fill=BLACK, bold=0):
|
||||||
self._draw(xr - self._tw(s, ft) - bold, y, s, ft, fill, bold)
|
self._draw(xr - self._tw_b(s, ft, bold), y, s, ft, fill, bold, self._track(ft, bold))
|
||||||
|
|
||||||
def ctext(self, cx, y, s, ft, fill=BLACK, bold=0):
|
def ctext(self, cx, y, s, ft, fill=BLACK, bold=0):
|
||||||
self._draw(cx - (self._tw(s, ft) + bold) / 2, y, s, ft, fill, bold)
|
self._draw(cx - self._tw_b(s, ft, bold) / 2, y, s, ft, fill, bold, self._track(ft, bold))
|
||||||
|
|
||||||
def hdash(self, x0, x1, y, dash=4, gap=4, fill=BLACK, width=1):
|
def hdash(self, x0, x1, y, dash=4, gap=4, fill=BLACK, width=1):
|
||||||
x = x0
|
x = x0
|
||||||
@@ -108,8 +119,8 @@ class DashboardRenderer:
|
|||||||
"""环比/同比一行的总宽度(标签 + 间隙 + 三角 + 数值),供居中/右对齐排布。"""
|
"""环比/同比一行的总宽度(标签 + 间隙 + 三角 + 数值),供居中/右对齐排布。"""
|
||||||
direction, val = pair
|
direction, val = pair
|
||||||
k = self.k
|
k = self.k
|
||||||
trw, tgap, vb = (8 * k, 4 * k, k) if direction else (0, 0, 0) # vb:数值伪粗体的偏移宽度(占位 -- 不加粗)
|
trw, tgap, vb = (8 * k, 4 * k, k) if direction else (0, 0, 0) # vb:数值伪粗体(占位 -- 不加粗)
|
||||||
return self._tw(label, self.F12) + 5 * k + trw + tgap + self._tw(val, self.F12) + vb
|
return self._tw(label, self.F12) + 5 * k + trw + tgap + self._tw_b(val, self.F12, vb)
|
||||||
|
|
||||||
def _cmp_line(self, cx, y, label, pair, fg=BLACK):
|
def _cmp_line(self, cx, y, label, pair, fg=BLACK):
|
||||||
"""环比/同比一行:标签 + 涨跌三角 + 数值,整体居中于 cx。direction 为 None 时不画三角。"""
|
"""环比/同比一行:标签 + 涨跌三角 + 数值,整体居中于 cx。direction 为 None 时不画三角。"""
|
||||||
@@ -151,9 +162,9 @@ class DashboardRenderer:
|
|||||||
k = self.k
|
k = self.k
|
||||||
GAP = 1 * k # 小号段与主数字的间距
|
GAP = 1 * k # 小号段与主数字的间距
|
||||||
sb = bold # 小号段同样伪粗体,否则挨着粗体主数字显得单薄
|
sb = bold # 小号段同样伪粗体,否则挨着粗体主数字显得单薄
|
||||||
wp = self._tw(pre, self.F12) + sb + GAP if pre else 0
|
wp = self._tw_b(pre, self.F12, sb) + GAP if pre else 0
|
||||||
ws = GAP + self._tw(suf, self.F12) + sb if suf else 0
|
ws = GAP + self._tw_b(suf, self.F12, sb) if suf else 0
|
||||||
wn = self._tw(num, self.F24) + bold
|
wn = self._tw_b(num, self.F24, bold)
|
||||||
x = cx - (wp + wn + ws) / 2.0
|
x = cx - (wp + wn + ws) / 2.0
|
||||||
# 基线对齐:F12/F24 数字墨迹底沿分别为 13k/26k(getbbox),小号段下移其差值
|
# 基线对齐:F12/F24 数字墨迹底沿分别为 13k/26k(getbbox),小号段下移其差值
|
||||||
ys = y + (self.F24.getbbox("0")[3] - self.F12.getbbox("0")[3])
|
ys = y + (self.F24.getbbox("0")[3] - self.F12.getbbox("0")[3])
|
||||||
@@ -182,6 +193,7 @@ class DashboardRenderer:
|
|||||||
track:见 _tw,用于给紧贴的字符手动加间距。"""
|
track:见 _tw,用于给紧贴的字符手动加间距。"""
|
||||||
_, t, _, b = ft.getbbox(s) # 该串墨迹在字身框内的上/下沿
|
_, t, _, b = ft.getbbox(s) # 该串墨迹在字身框内的上/下沿
|
||||||
y = cy - (t + b) / 2.0
|
y = cy - (t + b) / 2.0
|
||||||
|
track = self._track(ft, bold, track)
|
||||||
w = self._tw(s, ft, track)
|
w = self._tw(s, ft, track)
|
||||||
if align == "r":
|
if align == "r":
|
||||||
x -= w + bold
|
x -= w + bold
|
||||||
@@ -196,7 +208,7 @@ class DashboardRenderer:
|
|||||||
up = diff > 0
|
up = diff > 0
|
||||||
color = self._pace_color(diff)
|
color = self._pace_color(diff)
|
||||||
txt = f"{abs(int(diff)):02d}"
|
txt = f"{abs(int(diff)):02d}"
|
||||||
tw = self._tw(txt, self.FPACE)
|
tw = self._tw(txt, self.FPACE, self._track(self.FPACE, self.k)) # 宽版 FPACE=12px 加粗 → 含自动字距
|
||||||
self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r")
|
self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r")
|
||||||
TRI_GAP, TRI = 4 * self.k, 6 * self.k
|
TRI_GAP, TRI = 4 * self.k, 6 * self.k
|
||||||
cx = xr - tw - TRI_GAP - TRI / 2
|
cx = xr - tw - TRI_GAP - TRI / 2
|
||||||
@@ -234,7 +246,7 @@ class DashboardRenderer:
|
|||||||
pct_txt = f"{pct:02d}%" if ok else "--"
|
pct_txt = f"{pct:02d}%" if ok else "--"
|
||||||
self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=k, align="r", track=PCT_TRACK) # 百分比 F16(主)
|
self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=k, align="r", track=PCT_TRACK) # 百分比 F16(主)
|
||||||
bx0 = x0 + LABEL_W
|
bx0 = x0 + LABEL_W
|
||||||
bx1 = pct_r - self._tw(pct_txt, self.F16, PCT_TRACK) - G1
|
bx1 = pct_r - self._tw(pct_txt, self.F16, self._track(self.F16, k, PCT_TRACK)) - G1
|
||||||
by = cy - H // 2
|
by = cy - H // 2
|
||||||
self.d.rectangle([bx0, by, bx1, by + H], outline=outline, width=k) # 轨道边框随条色
|
self.d.rectangle([bx0, by, bx1, by + H], outline=outline, width=k) # 轨道边框随条色
|
||||||
fw = (bx1 - bx0) * pct / 100.0
|
fw = (bx1 - bx0) * pct / 100.0
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class E1002Renderer(DashboardRenderer):
|
|||||||
txt = f"{lvl}%" if lvl is not None else "--"
|
txt = f"{lvl}%" if lvl is not None else "--"
|
||||||
b = 1 if lvl is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗
|
b = 1 if lvl is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗
|
||||||
self._mid(x, cy, txt, F, bold=b)
|
self._mid(x, cy, txt, F, bold=b)
|
||||||
return x + self._tw(txt, F) + b
|
return x + self._tw_b(txt, F, b)
|
||||||
|
|
||||||
def _status_env(self, x, cy, dev):
|
def _status_env(self, x, cy, dev):
|
||||||
"""状态项·环境(F12,数字伪粗体,墨迹中心 cy):温度计(细管 3×8 + 底部球 7×7,管内水银线)+温度 | 蓝色水滴(上尖下圆)+湿度,
|
"""状态项·环境(F12,数字伪粗体,墨迹中心 cy):温度计(细管 3×8 + 底部球 7×7,管内水银线)+温度 | 蓝色水滴(上尖下圆)+湿度,
|
||||||
@@ -125,7 +125,7 @@ class E1002Renderer(DashboardRenderer):
|
|||||||
t = dev.get("temp")
|
t = dev.get("temp")
|
||||||
txt = f"{t:.1f}℃" if t is not None else "--"
|
txt = f"{t:.1f}℃" if t is not None else "--"
|
||||||
b = 1 if t is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗
|
b = 1 if t is not None else 0 # 数字伪粗体、字号不变(2026-09-14 用户要求);占位不加粗
|
||||||
self._mid(x, cy, txt, F, bold=b); x += self._tw(txt, F) + b + GAP
|
self._mid(x, cy, txt, F, bold=b); x += self._tw_b(txt, F, b) + GAP
|
||||||
self.d.polygon([(x + 4, cy - 6), (x, cy + 2), (x + 8, cy + 2)], fill=BLUE)
|
self.d.polygon([(x + 4, cy - 6), (x, cy + 2), (x + 8, cy + 2)], fill=BLUE)
|
||||||
self.d.ellipse([x, cy - 1, x + 8, cy + 6], fill=BLUE)
|
self.d.ellipse([x, cy - 1, x + 8, cy + 6], fill=BLUE)
|
||||||
x += 9 + GAP_I
|
x += 9 + GAP_I
|
||||||
@@ -133,7 +133,7 @@ class E1002Renderer(DashboardRenderer):
|
|||||||
txt = f"{h:.0f}%" if h is not None else "--"
|
txt = f"{h:.0f}%" if h is not None else "--"
|
||||||
b = 1 if h is not None else 0
|
b = 1 if h is not None else 0
|
||||||
self._mid(x, cy, txt, F, bold=b)
|
self._mid(x, cy, txt, F, bold=b)
|
||||||
return x + self._tw(txt, F) + b
|
return x + self._tw_b(txt, F, b)
|
||||||
|
|
||||||
def _color_strip(self, y0, y1, x0=None, x1=None):
|
def _color_strip(self, y0, y1, x0=None, x1=None):
|
||||||
"""六色测试色条:六块等宽实心色块,白块加 1px 黑框以便可见。真机校准用。"""
|
"""六色测试色条:六块等宽实心色块,白块加 1px 黑框以便可见。真机校准用。"""
|
||||||
@@ -239,7 +239,7 @@ class E1002Renderer(DashboardRenderer):
|
|||||||
col = GREEN if chg >= 0 else RED
|
col = GREEN if chg >= 0 else RED
|
||||||
xr = x1
|
xr = x1
|
||||||
pct_txt = f"{abs(chg):.2f}%"
|
pct_txt = f"{abs(chg):.2f}%"
|
||||||
self.rtext(xr, y0, pct_txt, self.F12, fill=col, bold=1); xr -= self._tw(pct_txt, self.F12) + 1 + 4 # 涨跌幅伪粗体(+1 为粗体偏移)
|
self.rtext(xr, y0, pct_txt, self.F12, fill=col, bold=1); xr -= self._tw_b(pct_txt, self.F12, 1) + 4 # 涨跌幅伪粗体
|
||||||
self.tri(xr - 4, y0 + 8.5, 7, up=chg >= 0, fill=col); xr -= 8 + 6
|
self.tri(xr - 4, y0 + 8.5, 7, up=chg >= 0, fill=col); xr -= 8 + 6
|
||||||
if btc.get("chg_label"): # 涨跌幅口径(24h / 1d)
|
if btc.get("chg_label"): # 涨跌幅口径(24h / 1d)
|
||||||
self.rtext(xr, y0, btc["chg_label"], self.F12); xr -= self._tw(btc["chg_label"], self.F12) + 10
|
self.rtext(xr, y0, btc["chg_label"], self.F12); xr -= self._tw(btc["chg_label"], self.F12) + 10
|
||||||
|
|||||||
Reference in New Issue
Block a user