Если холст на Mac охватывает огромные координаты и мельчайшие детали, храните грубые координаты отдельно от локальных смещений. Вычитайте начало координат области просмотра до масштабирования и проверяйте точность при каждом преобразовании. В этом руководстве такой расчет строится на синтетических данных сцены.

Конкретная задача — разместить синтетический маркер на масштабируемом холсте рабочего стола, не потеряв его небольшое смещение из-за большого общего адреса в сцене. В примере рассматривается одна ось; примените тот же расчет независимо к другой оси. Результатом будут числовые координаты для рисования, чтобы вы могли проверить арифметику до подключения холста.

Почему небольшое перемещение исчезает при больших координатах?

В Swift Double — это 64-разрядный тип с плавающей запятой. Его диапазон позволяет представлять большие и малые величины, но это не означает, что он способен сохранять каждое крошечное приращение, добавленное к большому значению. Apple описывает этот тип и API преобразования в документации Double.

Двоичная точность типа double составляет 53 значащих двоичных разряда. Поэтому расстояние между представимыми значениями увеличивается вместе с их величиной. В справочнике Голдберга по арифметике описаны как это представление, так и то, как вычитание ранее округленных операндов может выявить утрату информации, произошедшую раньше. Что должен знать каждый специалист по информатике о вычислениях с плавающей запятой

Рассмотрим следующие специально подобранные для примера входные данные: индекс ячейки 2^40, ширина ячейки 1024 единиц сцены и смещение маркера 2^-30 единиц сцены. Объединение адреса ячейки и ширины дает абсолютную координату 2^50. При этой положительной координате соседние значения Double отстоят друг от друга на 0.25 единиц. Прибавление крошечного смещения округляет результат обратно до исходной координаты.

Поэтому последующее вычитание начала координат дает ноль. Умножение этого нуля на большой коэффициент масштабирования не вернет смещение. Информация была потеряна при формировании абсолютной координаты.

Используйте x.ulp, чтобы проверить шаг значений при той величине, которую вы действительно храните. Apple определяет его как единицу последнего разряда и описывает исключения, включая нечисловые бесконечные значения. Double.ulpOfOne задает шаг для единицы; это не универсальный абсолютный допуск для координат сцены. 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. При необходимости следуйте актуальным инструкциям Установка Swift — macOS. В Terminal проверьте установку и создайте небольшой исполняемый пакет, используя процесс из документации Создание инструмента командной строки.

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.addingProduct(b, c) для вычисления a + 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 для рисования тип только итоговые ближайшие координаты и сравнивайте погрешность преобразования с допустимой погрешностью в экранных координатах. Если эти итоговые значения по-прежнему огромны, сначала определите, должен ли объект находиться в текущей области просмотра.

Для стабильного холста нужно определить поддерживаемый диапазон: адреса целочисленных ячеек, ограниченные локальные смещения, интервал масштабирования и допустимую погрешность в экранных координатах. Сохраняйте детализацию до формирования больших значений, вычисляйте относительно близкого начала координат и проверяйте характерные смещения на границах этого диапазона.

Источники