對於跨越巨大座標與微小細節的 Mac 畫布,將粗略位置與局部偏移分開儲存。在套用縮放前先減去檢視原點,並在每次轉換時檢查精度。本教學使用合成場景資料建構該計算。
具體任務是在可縮放的桌面畫布上定位一個合成標記,而不會因其整體場景位址龐大而遺失其微小位移。範例處理單一軸向;將相同計算獨立套用至另一軸向即可。它產生數值繪圖座標,以便您在連接畫布前檢查運算過程。
為何微小移動在大座標處會消失?
Swift 的 Double 是 64 位元浮點類型。其範圍允許極大與極小的幅度,但這不代表它能保留加在大數值上的每個微小增量。Apple 在 Double 中說明該類型及其轉換 API。
二進位雙精度擁有 53 個有效二進位數字。因此,可表示值之間的間距隨幅度增長而增大。Goldberg 的算術參考文獻解釋了這種表示方式,以及減去先前已捨入的運算元如何暴露先前丟失的資訊。What Every Computer Scientist Should Know About Floating-Point Arithmetic
請看這些明確說明性的輸入:儲存格索引為 2^40、儲存格寬度為 1024 場景單位、標記偏移為 2^-30 場景單位。將儲存格位址與寬度結合產生絕對座標 2^50。在該正座標處,相鄰 Double 值相距 0.25 單位。加上微小偏移會捨入回原座標。
事後減去原點因此得到零。將該零乘以大縮放因子無法恢復偏移。損失發生在組裝絕對座標時。
使用 x.ulp 檢視您實際儲存的量級處的間距。Apple 將其定義為最後一位單位,並說明其例外情況(含非有限值)。Double.ulpOfOne 描述 1 處的間距;它不是場景座標的通用絕對容差。ulp
座標表示應保留什麼?
對於此畫布,以 Int64 儲存格索引與 Double 局部偏移表示每個軸向。位置概念上意為 cell × cellSize + local,但計算應避免建構該絕對浮點值。
從一開始就以此分離形式保存標記與檢視原點。將已捨入的絕對座標轉換為儲存格與偏移,無法重現遺失的細節。
以下為範例的設計假設,而非通用數值限制:
- 每個儲存格寬度恰為
1024場景單位。 - 儲存格索引使用
Int64;它們是位址或計數,而非浮點數量。 - 每個局部偏移為有限值,並位於
0..<1024範圍內。 - 詳細定位僅接受原點儲存格及其直接鄰居。
- 縮放為有限且正數的顯示單位/場景單位。
- 螢幕原點為合成繪圖座標
512。
計算相對座標為 cellDifference × cellSize + localDifference。關鍵在於先減去整數儲存格索引,再將差值轉為 Double。Apple 一般的整數轉換可能會捨入;Double(exactly:) 則在無法精確表示時失敗。Double
即使減去兩個有效的 Int64 值也可能超出該類型範圍。使用 subtractingReportingOverflow(_:) 並在使用結果前檢查其旗標。Apple 明確說明發生溢位時部分結果會被截斷。subtractingReportingOverflow(_:)
此佈局將大位址與局部細節獨立保留。其精度仍取決於局部偏移:極微小的增量加在足夠大的局部偏移上時仍可能消失。根據目前檢視所需的最小細節選擇儲存格寬度與局部錨點。
在 Mac 上執行完整的本地計算
您需要可用的 Swift 工具鏈。如有必要,請遵循最新的 Install Swift — macOS 指示。在 Terminal 中檢查安裝,並使用 Build a Command-line Tool 所述工作流程建立一個小型可執行套件。
swift --version
mkdir SceneCoordinates
cd SceneCoordinates
swift package init --name SceneCoordinates --type executable
在 Sources 下產生的可執行目標中,以下方完整程式取代其進入點 Swift 檔案。使用包含此程式的單一進入點檔案,不含額外的最上層陳述式。它僅匯入 Swift 標準庫,不需套件相依性。
import Swift
struct AxisPosition {
let cell: Int64
let local: Double
}
enum CoordinateError: Error {
case invalidInput
case cellDifferenceOverflow
case outsideDetailedView
case inexactCellConversion
case unrepresentableResult
}
func screenCoordinate(
point: AxisPosition,
origin: AxisPosition,
zoom: Double
) throws -> Double {
let cellSize = 1024.0
let screenOrigin = 512.0
guard point.local.isFinite,
origin.local.isFinite,
point.local >= 0, point.local < cellSize,
origin.local >= 0, origin.local < cellSize,
zoom.isFinite, zoom > 0 else {
throw CoordinateError.invalidInput
}
let difference = point.cell.subtractingReportingOverflow(origin.cell)
guard !difference.overflow else {
throw CoordinateError.cellDifferenceOverflow
}
guard (-1...1).contains(difference.partialValue) else {
throw CoordinateError.outsideDetailedView
}
guard let cells = Double(exactly: difference.partialValue) else {
throw CoordinateError.inexactCellConversion
}
let localDifference = point.local - origin.local
let delta = localDifference.addingProduct(cells, cellSize)
let scaled = delta * zoom
guard delta.isFinite, scaled.isFinite,
delta == 0 || scaled != 0 else {
throw CoordinateError.unrepresentableResult
}
let screen = screenOrigin.addingProduct(delta, zoom)
guard screen.isFinite,
delta == 0 || screen != screenOrigin else {
throw CoordinateError.unrepresentableResult
}
return screen
}
@main
struct SceneCoordinates {
static func main() throws {
let cell: Int64 = 1_099_511_627_776
let step = Double(sign: .plus, exponent: -30, significand: 1)
let zoom = Double(sign: .plus, exponent: 30, significand: 1)
let absoluteBase = Double(cell) * 1024.0
let collapsed = (absoluteBase + step) - absoluteBase
let screen = try screenCoordinate(
point: AxisPosition(cell: cell, local: step),
origin: AxisPosition(cell: cell, local: 0),
zoom: zoom
)
let tiny = Double(sign: .plus, exponent: -600, significand: 1)
let largeZoom = Double(sign: .plus, exponent: 600, significand: 1)
let tinyScreen = try screenCoordinate(
point: AxisPosition(cell: 0, local: tiny),
origin: AxisPosition(cell: 0, local: 0),
zoom: largeZoom
)
precondition(absoluteBase.ulp == 0.25)
precondition(collapsed == 0)
precondition(screen == 513)
precondition(tinyScreen == 513)
print("Absolute spacing:", absoluteBase.ulp)
print("Absolute displacement:", collapsed)
print("Split-coordinate screen:", screen)
print("Tiny-coordinate screen:", tinyScreen)
}
}
從其目錄執行套件:
swift run SceneCoordinates
預期數值結果分別為 0.25、0.0、513.0 與 513.0。這些是針對提供的合成輸入所得出的結果。底層 binary64 間距、折疊位移與二次方縮放已在研究環境以 Python 驗證;該環境無 Swift 編譯器,故 Swift 程式僅經檢視未實際執行。
第一個穩定結果將 2^-30 與龐大儲存格位址分開保留,再以 2^30 縮放產生一顯示單位。第二個使用 2^-600 與縮放 2^600;兩輸入及其乘積在本例中均可表示。單純幅度小不代表會丟失精度。
程式對 a + b × c 使用 a.addingProduct(b, c)。Apple 將其說明為無中間捨入的融合運算。它減少該運算內的捨入;局部減法與任何先前的輸入捨入仍屬分開。addingProduct(::)
如何選擇實用的精度預算?
從目前縮放下必須保持可見的最小間距著手。以四分之一顯示單位為說明性目標,將 0.25 除以縮放以取得場景空間誤差預算。然後檢查計算所用的局部座標與中間值處的間距。
例如,縮放 2^30 時,說明性預算為 2^-32 場景單位。在局部座標 512 處,Double 間距為 2^-43,遠比該預算精細。在絕對座標 2^50 處,間距為 0.25,過於粗糙。這些計算值解釋了為何鄰近的局部錨點有助於此特定檢視。
間距是診斷指標,而非完整誤差界限。輸入轉換、減法、變換與最終繪圖轉換各自可能貢獻誤差。在接受縮放範圍前,請將已知位移通過包含實際渲染器的完整路徑進行測試。
| 觀察 | 可能的數值問題 | 下一步行動 |
|---|---|---|
| 加入偏移後儲存座標未變 | 儲存間距超過增量 | 將偏移分開保留或選擇更近的錨點 |
| 相對座標在轉換前正常 | 目標精度不足 | 在原點減法與縮放後再轉換;檢查產生的誤差 |
| 數值變為無限大 | 運算超出有限範圍 | 改變求值順序或限制支援的檢視範圍 |
| 非零位移變為零 | 向下溢位或捨入將其抹除 | 提早重新縮放或變更局部表示 |
| 遠端儲存格被此程式拒絕 | 超出詳細檢視策略 | 選擇鄰近原點或使用單獨的粗略概覽計算 |
對於概覽,某些微小細節合法地對應到遠小於一顯示單位的大小。請明確定義該行為。範例的鄰近儲存格限制刻意界定了詳細算術;放寬它需要新的精度預算,而非單純刪除防護。
在極小尺度與繪圖時會發生什麼?
Apple 區分 leastNormalMagnitude 與更小的正次正規值。次正規值相對正規值精度降低,因此最小正數不是良好的預設工作尺度。檢查中間結果與輸入,當將微小偏移乘以縮放時。leastNormalMagnitude
範例拒絕縮放為零或與螢幕原點無法區分的非零位移。它不針對視覺容差驗證每個非零結果。請為您的畫布加入基於容差的接受檢查,並顯式處理拋出的錯誤,而非以原點座標替代。
對於二維畫布,為每個軸向保留分離位置並在繪圖前計算兩個相對座標。對於三維擴充,Apple 提供 simd_double3,即 SIMD3<Double> 的別名。每個分量仍為 64 位元浮點值,有其各自的精度限制。simd_double3
將分離場景狀態作為權威位置。僅將最終鄰近繪圖座標轉換為繪圖 API 要求的類型,並依顯示空間預算測量轉換誤差。若那些最終值仍然龐大,先判斷物件是否屬於目前視口。
穩定畫布需要宣告支援範圍:整數儲存格位址、有界局部偏移、縮放區間與可接受的顯示空間誤差。在組裝大數值前保留細節、相對於鄰近原點計算、並在該範圍極限處驗證代表性位移。
來源
- Double — Apple Developer Documentation
- ulp — Apple Developer Documentation
- subtractingReportingOverflow(_:) — Apple Developer Documentation
- addingProduct(::) — Apple Developer Documentation
- leastNormalMagnitude — Apple Developer Documentation
- simd_double3 — Apple Developer Documentation
- Install Swift — Swift.org
- Build a Command-line Tool — Swift.org
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — Oracle