雲端 Mac 上的 iOS 多邊形選取驗收:邊界與孔洞

雲端 Mac 上的 iOS 多邊形選取驗收:邊界與孔洞

雲端 Mac 上的 iOS 多邊形選取驗收:邊界與孔洞

地圖選取區域看起來挖了一個洞,點選洞內卻仍然被選中;畫布上的點恰好落在線段上,不同縮放比例下又得到不同結果。這類問題通常不是遠端桌面點選不準,而是繪製、座標轉換與幾何判定採用了不同的規則約定。可以先在雲端 Mac 上執行純 Swift 迴歸測試腳本,將幾何問題與介面問題分開處理。

先定義三態結果與座標單位

不要一開始就回傳布林值。先將結果定義為內部、外部、邊界三種狀態,再決定邊界是否算選中。地圖編輯器可能允許選取邊緣,排除區域則可能連邊界都不允許進入,兩者不能共用未明確說明的布林判定規則。

本文處理的是同一平面座標系內、由直線段構成的簡單封閉環,允許外環內含孔洞;不處理經緯度的直接投影、自相交修復或曲線距離。傳入的點與頂點必須經過相同的座標轉換。

幾何容差不是觸控熱區。前者用來穩定數值分類,後者屬於互動設計;不要藉由放大容差,暗中擴大選取區域。

若座標採用畫布邏輯單位,容差也必須使用相同單位。螢幕上固定的點選半徑,必須透過反向變換換算為畫布座標;若有非等比縮放,就不能只除以單一縮放倍率。

以點到線段的距離辨識邊界

先檢查邊界,再呼叫路徑包含判定。計算投影位置時,必須將它限制在線段兩端之間,否則可能把點誤判為靠近無限延伸的直線。重複頂點若產生零長度邊,就改以點到點的距離處理。

將以下內容儲存為 polygon-check.swift。範例容差為 0.000001 個座標單位,只適用於這組固定測試資料,並非所有地圖或畫布都通用的精度。

import Foundation
import CoreGraphics

enum Location: Equatable {
    case inside, outside, boundary
}

func distance(_ p: CGPoint, _ a: CGPoint, _ b: CGPoint) -> CGFloat {
    let dx = b.x - a.x
    let dy = b.y - a.y
    let lengthSquared = dx * dx + dy * dy
    if lengthSquared == 0 {
        return hypot(p.x - a.x, p.y - a.y)
    }
    let projection = ((p.x - a.x) * dx + (p.y - a.y) * dy)
        / lengthSquared
    let t = min(1, max(0, projection))
    return hypot(p.x - a.x - t * dx, p.y - a.y - t * dy)
}

func locate(
    _ point: CGPoint,
    rings: [[CGPoint]],
    rule: CGPathFillRule,
    epsilon: CGFloat
) -> Location {
    precondition(epsilon.isFinite && epsilon >= 0)
    precondition(point.x.isFinite && point.y.isFinite)
    precondition(!rings.isEmpty)

    let path = CGMutablePath()
    for ring in rings {
        precondition(ring.count >= 3)
        precondition(ring.allSatisfy { $0.x.isFinite && $0.y.isFinite })
        for i in ring.indices {
            let next = (i + 1) % ring.count
            if distance(point, ring[i], ring[next]) <= epsilon {
                return .boundary
            }
        }
        path.move(to: ring[0])
        for vertex in ring.dropFirst() {
            path.addLine(to: vertex)
        }
        path.closeSubpath()
    }
    return path.contains(point, using: rule) ? .inside : .outside
}

這些前置條件適用於已通過輸入驗證的測試資料。正式環境的程式碼處理匯入檔案時,應回傳可妥善處理的驗證錯誤,而不是讓無效座標導致程序終止。也需要限制座標的數值範圍,避免平方運算溢位;輸入值有限,不代表所有中間運算結果都有限。

固定孔洞測試資料並比較填色規則

接著附加以下程式碼。內外環採用相同的繞行方向,因此非零環繞數規則會填滿內環;奇偶規則則將內環區域視為孔洞。兩種結果都符合各自的規則,不能只憑螢幕截圖就認定幾何函式庫出錯。

let outer: [CGPoint] = [
    CGPoint(x: 0, y: 0), CGPoint(x: 10, y: 0),
    CGPoint(x: 10, y: 10), CGPoint(x: 0, y: 10)
]
let hole: [CGPoint] = [
    CGPoint(x: 3, y: 3), CGPoint(x: 7, y: 3),
    CGPoint(x: 7, y: 7), CGPoint(x: 3, y: 7)
]
let rings = [outer, hole]
let epsilon: CGFloat = 0.000001

func check(_ x: CGFloat, _ y: CGFloat, _ expected: Location) {
    precondition(
        locate(
            CGPoint(x: x, y: y),
            rings: rings,
            rule: .evenOdd,
            epsilon: epsilon
        ) == expected
    )
}

check(1, 1, .inside)
check(5, 5, .outside)
check(12, 5, .outside)
check(0, 5, .boundary)
check(3, 5, .boundary)
check(0, 0, .boundary)
check(-epsilon / 2, 5, .boundary)
check(-epsilon * 2, 5, .outside)

precondition(
    locate(
        CGPoint(x: 5, y: 5),
        rings: rings,
        rule: .winding,
        epsilon: epsilon
    ) == .inside
)

在節點的終端機執行:

xcrun --find swift
xcrun swift polygon-check.swift

正常結束且結束代碼為零,代表這些斷言全部通過;任一斷言失敗,都會以非零代碼結束。執行這份驗收腳本時,不要使用會停用斷言檢查的最佳化方式。

補上方向、變換與退化案例

矩形只能驗證最基本的路徑。迴歸測試集至少還要加入凹多邊形、水平邊、連續共線頂點,以及位於頂點前後的兩個鄰近點。凹口內的點應維持判定為外部,不能因為落在外接矩形內就被選中。

使用奇偶規則時,將任意一個環的頂點順序反轉後,所有非邊界樣本的分類都應維持不變。將所有頂點與測試點一起平移,也應得到原本的結果。若整體等比放大,容差也必須同步放大,否則比較的就不再是同一個邊界帶。

少於三個相異頂點、整個環共線、自相交,以及孔洞超出外環等情況,都需要在輸入階段個別拒絕,或明確定義處理方式。範例中的頂點數量檢查,不等於有效多邊形檢查。不要自動刪除異常點後繼續測試,否則可能將使用者輸入改造成另一塊區域。

記錄可重現的失敗案例

失敗紀錄應保存環的頂點順序、點座標、填色規則、容差與變換矩陣。儲存浮點值時要保留足夠精度,不要只留下四捨五入後的介面座標;後者可能讓邊界兩側的點變成同一個數值。

接回 iOS 介面時,只排查座標轉換鏈

純幾何測試資料全部通過後,再將同一個判定函式接回應用程式。逐層確認觸控點是否已從視圖座標轉換為路徑座標、捲動偏移量是否被重複扣除,以及路徑繪製與命中測試是否採用相同的填色規則。

縮放前後選取同一個邏輯點,結果應該一致;若不一致,應優先檢查反向變換,而不是增加容差。描邊寬度也不等於填色區域:若需要讓描邊可被點選,就應另外建立描邊的命中判定邏輯。

最後保留兩類測試:純 Swift 腳本負責驗證幾何規則約定,少量介面測試負責驗證座標對應。前者適合在遠端 Mac 上於每次提交時執行,後者則驗證實際的互動流程。這樣一旦失敗,就能先確定問題出在資料、規則還是視圖轉換,不必反覆猜測點選位置。

常見問題

為什麼不能直接以 CGPath.contains 作為最終選取結果?

邊界是否納入選取是產品規則。應先檢查點到線段的距離,區分內部、外部與邊界,再交由互動層決定邊界行為。

孔洞的頂點一定要反向排列嗎?

採用奇偶填色時,環的方向不決定孔洞結果;採用非零環繞規則時,方向則會影響結果。繪製與選取測試必須使用相同規則。

用實際工作,測試遠端 Mac。

租用一台專屬使用的 Mac mini M4,透過 macOS 圖形介面與命令列操作。適合先測試連線和工作流程;若需要長時間離線運作或連接專業硬體,也可以評估自行購買。

租用專屬 Mac mini

先租一天,再決定要不要買。

實際測試連線體驗和工作任務,再選擇適合自己的使用週期。