iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI 自動化

用 AI Agent 打造你的產品使用手冊產線系列 第 12

[Day 12] 產線實作 5:標號與標號說明

  • 分享至 

  • xImage
  •  

昨天把畫框標號的疊層機制做出來了,也在最後留下一個沒解決的問題:圓標會擋到旁邊的東西。今天就來處理這件事,順便把「圖上的數字要怎麼跟正文對得起來」一起講完。

圓標放錯位置會發生什麼事

原本的擺放邏輯寫死在左上角外側,如果是空曠處就沒問題,但如果旁邊有東西時會出三種狀況:

  1. 蓋住目標本身:讀者看不到真正要看的東西
  2. 兩個圓標疊在一起:相鄰元件各標一個,分不清誰是誰
  3. 超出畫布邊界被裁掉:最容易發生、也最難自己發現

這些都不會讓程式報錯,使用手冊一樣可以建立,只是品質沒那麼好,會讓人覺得不夠細心。

擺放演算法

要解決這個問題,方法也不複雜。只要準備一串候選位置,依好看到將就排序,依序檢查,通過就用:

const R = 14 // 圓標半徑

/** 候選圓心,依序:外側角 → 內側角 → 左右中點。 */
function candidates(box: Rect) {
  const { x, y, width: w, height: h } = box
  return [
    { cx: x, cy: y },                       // 左上角外側(預設)
    { cx: x + w, cy: y },                   // 右上角
    { cx: x, cy: y + h },                   // 左下角
    { cx: x + w, cy: y + h },               // 右下角
    { cx: x + R, cy: y + R },               // 左上角內側
    { cx: x + w - R, cy: y + R },           // 右上角內側
    { cx: x + R, cy: y + h - R },           // 左下角內側
    { cx: x + w - R, cy: y + h - R },       // 右下角內側
    { cx: x - R, cy: y + h / 2 },           // 左側
    { cx: x + w + R, cy: y + h / 2 },       // 右側
  ]
}

接著就是三條規則依序檢查:不超出畫布 → 不撞到已放好的圓標 → 不蓋到別的元件

for (const item of items) {
  const box = pad(item)

  // 自己的元件不算障礙物:圓標壓在自己框角上是慣例。
  const others = obstacles.filter((o) => o.testid !== item.testid)

  const hit = candidates(box).find((c) => {
    const r = badgeRect(c.cx, c.cy)
    return inside(r, canvas) && !taken.some((t) => overlaps(r, t)) && !others.some((o) => overlaps(r, o))
  })

  // 全部候選都不行:退回預設位置並夾回畫布內
  const { cx, cy } = hit ?? clampIntoCanvas(candidates(box)[0], canvas)

  taken.push(badgeRect(cx, cy))
  out.push({ ...item, cx, cy })
}

規則要依序檢查完,不能只做第一步。以下分享幾個例子。

出界:圓標被裁掉

範例的統計卡靠著面板左上角,clip 只留 8px padding,比圓標半徑還小,預設位置讓三個圓標都被切掉一角:

預設擺放,三個圓標都被畫布邊界裁掉

套上規則後,翻到「左上角內側」,圓標全部落在卡片裡:

圓標翻到內側,完整落在畫布內

容易寫錯的地方:畫布不是 viewport,是 clip 的範圍。如果用 viewportSize() 當依據,上面的圓標就會被判定為「沒出界」,因為它們確實在視窗裡,只是不在截圖裡。

碰撞:圓標蓋到隔壁元件

對話框裡 ③ 壓到「取消」按鈕,把它也標註後,④ 直接騎在 ③ 的框上:

預設擺放,④ 的圓標壓在 ③(取消按鈕)的框上

「取消」有 data-testid,進了障礙物清單,④ 的左上角外側被否決,改右上角:

④ 移到儲存按鈕的右上角,兩個圓標都清楚

障礙物清單從哪裡來

「障礙物」是畫面上其他看得到的元件,每個要操作的元件都已補了 data-testid(Day 08),直接拿來用:

const obstacles = await page.evaluate(() =>
  [...document.querySelectorAll('[data-testid]')]
    // 只留葉節點:容器類 testid 範圍太大,會讓所有位置都不能用
    .filter((el) => !el.querySelector('[data-testid]'))
    // 只留真的在最上層的元件
    .filter((el) => {
      const r = el.getBoundingClientRect()
      if (r.width === 0 || r.height === 0) return false
      const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)
      return top !== null && (top === el || el.contains(top))
    })
    .map((el) => ({ testid: el.dataset.testid, ...el.getBoundingClientRect().toJSON() })),
)

第二個 filter 是實際跑過才補上的:一開始只做葉節點過濾,④ 怎麼都不肯移動 —— 原因是遮罩蓋住的格子也被算成障礙物,它們在 DOM 裡有尺寸但看不到,導致每個位置都撞到東西。

這正是 Day 08 講過的:元件存在 DOM 不代表看得到。用 elementFromPoint() 對中心點做命中測試就能濾掉。

編號順序

編號可按「閱讀順序」或「操作順序」排,就看對應的段落是在「介紹介面」還是「說明步驟」。

在設定檔裡,這個順序就是 annotate 陣列裡目標的排列順序,不需要額外開一個欄位來表達。

標號說明要放哪裡

標號說明 (legend) 常見放法:圖片內部、圖片下方列出、或穿插正文引用,取捨在翻譯成本、版面與維護性。

這系列的選擇:legend 寫在設定檔裡,渲染成圖片下方一張小表格。

{
  "screenshot": "camera-dialog-01.png",
  "annotate": [
    { "key": "name",    "testid": "camera-dialog-name",    "legend": "攝影機顯示名稱" },
    { "key": "confirm", "testid": "camera-dialog-confirm", "legend": "儲存按鈕" }
  ]
}

渲染出來就是「正文 + 圖 + 圖說 + legend 表格」:

手冊成品示意:正文、截圖與 legend 表格

這樣做的最大優點是:翻譯不用重新截圖,換 legend 文字即可。使用手冊翻譯這件事會在後面找機會討論。

legend 基本上都是用名詞短語,不用祈使句 (e.g. 寫「儲存按鈕」而非「點這裡儲存」),操作說明是正文的事,legend 只負責標明「這是什麼」,也建議設字數上限。

正文不要硬寫數字

有一個蠻常見的問題就是標號更新。原本 1、2、3,插入新元件變 1、2、3、4,正文的「編號 3」沒跟著改,讀者會卡住。

解決辦法就是 正文不寫數字,改引用 legend 的語意 key:

在 {{legend.name}} 填入攝影機名稱,確認無誤後按 {{legend.confirm}}。

渲染時才換成編號,插入新標註只讓數字重算,既有引用不會錯位。

這些規則可寫成腳本自動檢查,不用每次都靠肉眼核對:

  • 編號連續、無跳號或重複
  • legend 的每個 key,annotate 都有對應目標
  • 正文引用的 key,在該章節 legend 裡存在

經驗分享

1. 演算法一定有解不開的時候

候選位置全被否決是有可能會發生的事,例如:元件密集或 clip 很緊時,此時就乖乖退回預設位置夾回畫布內。可以的話,要留手動覆寫欄位(例如 "badge": "top-right"),讓人可以對難處理的圖直接指定。

2. 中英文長度差會撐壞 legend 排版

有時候,中文 legend 6 個字,英文卻要 20 幾個字元。或者是,固定寬度表格對中文剛好,但是對英文卻要換行,變得難看。遇到這種狀況就只能先人工處理了 (排版彈性留到後面再找機會展開討論)。

到目前為止,一張時機、範圍、標註都正確的截圖就差不多完成了。剩下一個小部分就是「敏感資訊的遮蔽」,明天會再繼續接著介紹。


上一篇
[Day 11] 產線實作 4:用 CSS 畫框與標註
下一篇
[Day 13] 產線實作 6:遮蔽敏感資訊
系列文
用 AI Agent 打造你的產品使用手冊產線17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言