Skip to content

[core] Разделить физическую валидность spring и бюджеты исполнителей #218

Description

@lemone112

Пользовательский результат

Физически корректная пружина описывается и вычисляется без скрытого изменения намерения. Ограничения frame-loop, compositor-компилятора и finite easing применяются только соответствующим исполнителем, а не маскируются под общую «валидность spring».

Подтверждённая проблема

Сейчас validateSpringParams() смешивает два разных закона:

  1. физическая валидность — конечные mass > 0, stiffness > 0, damping >= 0;
  2. представимость конкретным frame-loop — аналитическое время оседания должно уложиться в MAX_FRAMES * FIXED_DT_S ≈ 33.3 s.

Из-за этого чистый аналитический spring(params, t) отвергает медленные или незатухающие, но математически корректные системы, хотя их значение в произвольный момент вычисляется замкнутой формой без scheduler.

fromBounce() и fromVisualDuration() дополнительно скрыто меняют параметры, чтобы пройти тот же executor-budget:

  • точное преобразование duration=100, bounce=0, mass=1 даёт ω₀=2π/100, ζ=1, k≈0.00394784176, c≈0.12566370614; текущий путь ускоряет систему до другого ω₀;
  • bounce=1 математически означает ζ=0, то есть damping=0; текущий путь превращает запрос в сильно ускоренную слабо затухающую систему, чтобы она искусственно «успела» осесть;
  • такое преобразование не является ни SwiftUI/Motion mapping, ни физической валидацией и не обратимо.

Математический закон

Канонические координаты формы движения:

ω₀ = sqrt(k / m)
ζ  = c / (2 sqrt(km))
v₀ = initial velocity

Для duration + bounce:

ω₀ = 2π / duration
ζ  = 1 - bounce
k  = m ω₀²
c  = 2 m ζ ω₀

Это точное преобразование. Масштабирование (m,k,c) -> (λm,λk,λc) не меняет ω₀, ζ и траекторию. Следовательно, mass в этой параметризации не является независимой perceptual-ручкой «тяжести».

Требуемая архитектура

1. Физическая граница

Ввести одну узкую проверку физического домена, например:

validateSpringPhysics(params)

Она проверяет только:

  • конечный mass > 0;
  • конечный stiffness > 0;
  • конечный damping >= 0.

Публичный чистый spring(params, t) использует только эту границу и должен вычислять любую физически валидную систему, включая:

  • очень медленную;
  • damping = 0;
  • систему, которая не имеет конечного settle horizon.

2. Executor-specific representability

Отдельные границы принадлежат исполнителям:

  • drive / MotionValue — может ли автономный frame-loop гарантированно завершиться в своём бюджете;
  • compositor/compiler — помещается ли кривая в BASE_GRID_MAX и допустимый синхронный compile-cost;
  • finite easing — существует ли конечный нормализованный горизонт;
  • прямой аналитический sampler — не имеет frame-budget.

Не создавать второй общий «валидатор на всё». Каждый executor использует собственный предикат/ошибку рядом со своей политикой.

3. Точные constructors

fromBounce() и fromVisualDuration() обязаны:

  • возвращать точное математическое преобразование запрошенных координат;
  • не изменять duration, visualDuration, bounce, ζ или ω₀ ради runtime-budget;
  • сохранять точные scale-invariance и round-trip свойства;
  • для bounce=1 честно возвращать damping=0.

Если продукту действительно нужен coercion-helper, он должен быть отдельным явно названным API и возвращать metadata о том, что изменено. Он не входит в constructors по умолчанию и не должен появиться без отдельного доказанного consumer-case.

4. Ошибки и migration

  • сохранить структурированные ошибки;
  • разделить «нефизические параметры» и «этот executor не может гарантированно завершить систему»;
  • обновить error catalog и migration note как pre-1.0 behavioral correction;
  • не добавлять semantic presets light/heavy/playful в engine.

RED / доказательства

  1. Characterization: обычные текущие параметры, которые не подвергаются coercion, остаются bit-exact.
  2. Exact mapping: property-тесты duration/bounce/mass -> ω₀/ζ -> k/c на широком конечном домене.
  3. Scale invariance: для конечного λ>0 траектории (m,k,c) и (λm,λk,λc) совпадают в ULP-допуске.
  4. Edge: bounce=1 возвращает damping=0; чистый spring() вычисляет конечные value/velocity, а finite/live executor fail-fast с executor-specific причиной.
  5. Slow spring: точный spring({m:1,k:1,c:1}, t) доступен; автономный runner отдельно решает, допускает ли его lifecycle-budget.
  6. No hidden mutation: constructor output сравнивается с независимой формулой, а не с production helper.
  7. Fuzz/mutation: три режима + ζ=0, scale extremes, near-critical и very-slow cases; мутанты, возвращающие старый clamp/coercion, обязаны погибать.
  8. Browser: runtime-paths, которых касается разделение policy, проходят Chromium/Firefox/WebKit; чистая математика остаётся headless.

Производительность и размер

  • per-frame путь не получает новых проверок;
  • параметры валидируются один раз на executor-boundary;
  • nano остаётся byte-identical и <=1024 B gzip;
  • существующие root/animate/compositor thresholds не повышаются;
  • удаление общего settle-check из чистого sampler не должно ухудшить import cost.

Не входит

Связано: #93, #105, #106.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions