Speichere bei einer Mac-Zeichenfläche mit riesigen Koordinaten und winzigen Details grobe Positionen getrennt von lokalen Offsets. Ziehe den Ursprung der Ansicht ab, bevor du den Zoom anwendest, und prüfe die Genauigkeit bei jeder Umwandlung. Dieses Tutorial führt diese Berechnung anhand synthetischer Szenendaten durch.
Die konkrete Aufgabe besteht darin, einen synthetischen Marker auf einer zoombaren Desktop-Zeichenfläche zu positionieren, ohne seine kleine Verschiebung zu verlieren, weil seine absolute Szenenadresse groß ist. Das Beispiel behandelt eine Achse; wende dieselbe Berechnung unabhängig auf die andere Achse an. Es liefert numerische Zeichenkoordinaten, damit du die Rechnung prüfen kannst, bevor du sie mit einer Zeichenfläche verbindest.
Warum verschwindet eine winzige Bewegung bei einer großen Koordinate?
Swifts Double ist ein 64-Bit-Gleitkommatyp. Sein Wertebereich umfasst große und kleine Größenordnungen, aber das bedeutet nicht, dass jede winzige zu einem großen Wert addierte Änderung erhalten bleibt. Apple dokumentiert den Typ und seine Umwandlungs-APIs in Double.
Die binäre Gleitkommadarstellung mit doppelter Genauigkeit hat 53 signifikante Binärstellen. Daher wächst der Abstand zwischen darstellbaren Werten mit ihrer Größenordnung. Goldbergs Referenz zur Arithmetik erläutert sowohl diese Darstellung als auch, wie die Subtraktion zuvor gerundeter Operanden Informationen offenlegen kann, die bereits früher verloren gingen. Was alle Informatiker über Gleitkommaarithmetik wissen sollten
Betrachte diese ausdrücklich als Beispiele gedachten Eingabewerte: einen Zellindex von 2^40, eine Zellbreite von 1024 Szeneneinheiten und einen Marker-Offset von 2^-30 Szeneneinheiten. Die Kombination aus Zelladresse und Breite ergibt eine absolute Koordinate von 2^50. An dieser positiven Koordinate liegen benachbarte Double-Werte 0.25 Einheiten auseinander. Durch das Addieren des winzigen Offsets wird auf die ursprüngliche Koordinate zurückgerundet.
Wenn du danach den Ursprung abziehst, erhältst du daher null. Eine Multiplikation dieser Null mit einem großen Zoomfaktor kann den Offset nicht wiederherstellen. Der Informationsverlust trat auf, als die absolute Koordinate zusammengesetzt wurde.
Verwende x.ulp, um den Abstand bei der Größenordnung zu untersuchen, die du tatsächlich speicherst. Apple definiert diesen als Einheit an der letzten Stelle und dokumentiert seine Ausnahmen, einschließlich nicht endlicher Werte. Double.ulpOfOne beschreibt den Abstand bei eins; der Wert ist keine allgemeingültige absolute Toleranz für Szenenkoordinaten. ulp
Welche Genauigkeit sollte die Koordinatendarstellung bewahren?
Stelle für diese Zeichenfläche jede Achse durch einen Int64-Zellindex und einen lokalen Double-Offset dar. Eine Position bedeutet konzeptionell Zelle × Zellgröße + lokal, aber bei der Berechnung sollte dieser absolute Gleitkommawert nicht erzeugt werden.
Speichere Marker und Ursprung der Ansicht von Anfang an in dieser getrennten Form. Wenn du eine bereits gerundete absolute Koordinate nachträglich in Zelle und Offset umwandelst, kannst du verlorene Details nicht wiederherstellen.
Für das durchgerechnete Beispiel gelten die folgenden Entwurfsannahmen; sie sind keine allgemeingültigen numerischen Grenzwerte:
- Jede Zelle ist genau
1024Szeneneinheiten breit. - Zellindizes verwenden
Int64; sie sind Adressen oder Zählwerte, keine Gleitkommawerte. - Jeder lokale Offset ist endlich und liegt in
0..<1024. - Die Detailpositionierung akzeptiert nur die Ursprungszelle und deren unmittelbare Nachbarzellen.
- Der Zoom ist eine endliche, positive Zahl von Anzeigeeinheiten pro Szeneneinheit.
- Der Bildschirmursprung ist die synthetische Zeichenkoordinate
512.
Berechne die relative Koordinate als Zellendifferenz × Zellgröße + lokaleDifferenz. Entscheidend ist, die ganzzahligen Zellindizes abzuziehen, bevor die Differenz in Double umgewandelt wird. Apples gewöhnliche Ganzzahlumwandlung kann runden; Double(exactly:) schlägt stattdessen fehl, wenn eine exakte Darstellung nicht möglich ist. Double
Auch die Subtraktion zweier gültiger Int64-Werte kann den Wertebereich dieses Typs überschreiten. Verwende subtractingReportingOverflow(_:) und prüfe das zugehörige Flag, bevor du das Ergebnis verwendest. Apple dokumentiert ausdrücklich, dass das Teilergebnis bei einem Überlauf abgeschnitten wird. subtractingReportingOverflow(_:)
Diese Darstellung bewahrt eine große Adresse unabhängig von lokalen Details. Die Genauigkeit hängt dennoch von den lokalen Offsets ab: Eine sehr kleine Änderung kann verschwinden, wenn sie zu einem ausreichend großen lokalen Offset addiert wird. Wähle Zellbreiten und lokale Anker anhand des kleinsten Details, das die aktuelle Ansicht benötigt.
Führe eine vollständige lokale Berechnung auf deinem Mac aus
Du benötigst eine funktionierende Swift-Toolchain. Folge bei Bedarf der aktuellen Anleitung Swift installieren — macOS. Prüfe in Terminal die Installation und erstelle mit dem in Ein Kommandozeilenwerkzeug erstellen dokumentierten Ablauf ein kleines ausführbares Paket.
swift --version
mkdir SceneCoordinates
cd SceneCoordinates
swift package init --name SceneCoordinates --type executable
Ersetze im erzeugten ausführbaren Ziel unter Sources die Swift-Einstiegsdatei durch das vollständige Programm unten. Verwende eine einzige Einstiegsdatei mit diesem Programm und ohne zusätzliche Anweisungen auf oberster Ebene. Es importiert nur Swifts Standardbibliothek und benötigt keine Paketabhängigkeiten.
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)
}
}
Führe das Paket in seinem Verzeichnis aus:
swift run SceneCoordinates
Die erwarteten numerischen Ergebnisse lauten jeweils 0.25, 0.0, 513.0 und 513.0. Dies sind aus den angegebenen synthetischen Eingaben abgeleitete Ergebnisse. Der zugrunde liegende binary64-Abstand, die verschwundene Verschiebung und die Skalierung mit Zweierpotenzen wurden in der Forschungsumgebung mit Python überprüft; dort war kein Swift-Compiler verfügbar, weshalb das Swift-Programm geprüft, aber nicht ausgeführt wurde.
Das erste stabile Ergebnis bewahrt 2^-30 getrennt von der riesigen Zelladresse auf und skaliert den Wert dann mit 2^30 auf eine Anzeigeeinheit. Beim zweiten wird 2^-600 mit einem Zoom von 2^600 verwendet; beide Eingaben und ihr Produkt sind in diesem Beispiel darstellbar. Eine kleine Größenordnung allein bedeutet nicht, dass Genauigkeit verloren geht.
Das Programm verwendet a.addingProduct(b, c) für a + b × c. Apple dokumentiert dies als eine Operation mit fusionierter Berechnung ohne Zwischenrundung. Sie verringert die Rundung innerhalb dieser Operation; die lokale Subtraktion und frühere Rundungen der Eingaben bleiben davon unberührt. addingProduct(::)
Wie legst du ein sinnvolles Genauigkeitsbudget fest?
Beginne mit dem kleinsten Abstand, der bei aktuellem Zoom sichtbar bleiben muss. Teile für ein beispielhaftes Ziel von einem Viertel einer Anzeigeeinheit 0.25 durch den Zoom, um ein Fehlerbudget im Szenenkoordinatenraum zu erhalten. Untersuche dann den Abstand bei den lokalen Koordinaten und Zwischenwerten, die in der Berechnung verwendet werden.
Beim Zoom 2^30 beträgt das beispielhafte Budget etwa 2^-32 Szeneneinheiten. An der lokalen Koordinate 512 beträgt der Double-Abstand 2^-43 und ist damit deutlich feiner als dieses Budget. An der absoluten Koordinate 2^50 beträgt der Abstand 0.25 und ist viel zu grob. Diese berechneten Werte erklären, weshalb ein nahe gelegener lokaler Anker dieser Ansicht hilft.
Der Abstand ist ein Diagnosewert, keine vollständige Fehlergrenze. Eingabeumwandlung, Subtraktion, Transformationen und die abschließende Umwandlung zum Zeichnen können jeweils Fehler beitragen. Prüfe bekannte Verschiebungen durch den gesamten Ablauf, einschließlich des tatsächlichen Renderers, bevor du einen Zoombereich freigibst.
| Beobachtung | Wahrscheinliches numerisches Problem | Nächster Schritt |
|---|---|---|
| Beim Addieren eines Offsets bleibt die gespeicherte Koordinate unverändert | Der Speicherabstand ist größer als die Änderung | Bewahre den Offset getrennt auf oder wähle einen näher gelegenen Anker |
| Relative Koordinaten funktionieren bis zur Umwandlung | Die Zielgenauigkeit reicht nicht aus | Wandle erst nach dem Abziehen des Ursprungs und Anwenden des Zooms um; prüfe den resultierenden Fehler |
| Werte werden unendlich | Eine Operation überschreitet den endlichen Wertebereich | Ändere die Reihenfolge der Berechnung oder begrenze die unterstützte Ansicht |
| Eine Verschiebung ungleich null wird zu null | Unterlauf oder Rundung hat sie ausgelöscht | Skaliere früher um oder ändere die lokale Darstellung |
| Dieses Programm weist eine entfernte Zelle zurück | Die Richtlinie für Detailansichten wurde überschritten | Wähle einen nahe gelegenen Ursprung oder verwende eine separate grobe Berechnung für die Übersicht |
Bei einer Übersicht kann es sinnvoll sein, dass manche winzigen Details auf deutlich weniger als eine Anzeigeeinheit abgebildet werden. Lege dieses Verhalten ausdrücklich fest. Die Beschränkung auf Nachbarzellen im Beispiel begrenzt absichtlich die Berechnung für Detailansichten; eine Erweiterung erfordert ein neues Genauigkeitsbudget, statt einfach die Schutzprüfung zu entfernen.
Was passiert bei sehr kleinen Maßstäben und beim Zeichnen?
Apple unterscheidet leastNormalMagnitude von noch kleineren positiven subnormalen Werten. Subnormale Werte haben gegenüber normalen Werten eine verringerte Genauigkeit. Daher eignet sich die kleinste positive Zahl nicht als üblicher Arbeitsmaßstab. Prüfe beim Multiplizieren winziger Offsets mit dem Zoom auch Zwischenergebnisse und nicht nur die Eingaben. leastNormalMagnitude
Das Beispiel weist eine Verschiebung ungleich null zurück, wenn sie durch Skalierung zu null wird oder sich nicht mehr vom Bildschirmursprung unterscheiden lässt. Es garantiert nicht, dass jedes von null verschiedene Ergebnis innerhalb einer visuellen Toleranz liegt. Ergänze eine für deine Zeichenfläche passende Toleranzprüfung und behandle ausgelöste Fehler ausdrücklich, statt ersatzweise die Ursprungkoordinate einzusetzen.
Bewahre bei einer zweidimensionalen Zeichenfläche für jede Achse eine getrennte Position und berechne beide relativen Koordinaten vor dem Zeichnen. Für eine dreidimensionale Erweiterung stellt Apple simd_double3 bereit, einen Alias für SIMD3<Double>. Jede Komponente bleibt ein 64-Bit-Gleitkommawert mit einer eigenen Genauigkeitsgrenze. simd_double3
Verwende den getrennten Szenenzustand als maßgebliche Position. Wandle nur die abschließenden, nahe gelegenen Zeichenkoordinaten in den vom Zeichen-API benötigten Typ um und vergleiche den Umwandlungsfehler mit dem Budget im Anzeigeraum. Wenn diese Endwerte immer noch riesig sind, prüfe zuerst, ob das Objekt zur aktuellen Ansicht gehört.
Eine stabile Zeichenfläche benötigt einen festgelegten unterstützten Bereich: ganzzahlige Zelladressen, begrenzte lokale Offsets, einen Zoomintervall und einen akzeptablen Fehler im Anzeigeraum. Bewahre Details, bevor du große Werte zusammensetzt, berechne relativ zu einem nahe gelegenen Ursprung und prüfe repräsentative Verschiebungen an den Grenzen dieses Bereichs.
Quellen
- 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
- Swift installieren — Swift.org
- Ein Kommandozeilenwerkzeug erstellen — Swift.org
- Was alle Informatiker über Gleitkommaarithmetik wissen sollten — Oracle