Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
### Добавлено
- Код `LM169`: `springAsEasing` получила пружину без затухания, у которой финитной
проекции на [0,1] не существует.
- `compileSpringPlan` принимает `maxValueError`: абсолютный
бюджет ошибки в единицах numeric `from/to` переводится в нормализованный допуск
до кэша и сегментера; более строгий из `tolerance` и бюджета результата побеждает.
- Код `LM172`: `maxValueError` не является положительным конечным числом.

## [0.3.0] — 2026-08-04

Expand Down
6 changes: 3 additions & 3 deletions conformance/motion-program-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2440,7 +2440,7 @@
"damping": 26,
"v0": 3,
"compilerTolerance": 0.0025,
"durationMs": 772.2521946132229
"durationMs": 1038.8472640593557
}
],
"valid": [
Expand Down Expand Up @@ -2596,8 +2596,8 @@
{
"name": "compiled-v0-spring",
"categories": ["curve", "compiled-origin", "spring"],
"program": [1, 0, [], [0, [1, 0, 0, 0.0018248175182481751, 0.004227657999707424, 0.0146, 0.039, 0.032850000000000004, 0.0987, 0.051089999999999997, 0.1656, 0.09853999999999999, 0.3465, 0.12774, 0.4508, 0.14234, 0.4991, 0.15693, 0.5445, 0.18248, 0.6166, 0.19707999999999998, 0.6537, 0.21533000000000002, 0.6959, 0.23358, 0.7337, 0.25182, 0.7675, 0.27007000000000003, 0.7974, 0.29197, 0.8288, 0.31022, 0.8515, 0.33212, 0.8751, 0.35401000000000005, 0.8952, 0.37591, 0.9123, 0.39781, 0.9267, 0.42335999999999996, 0.9407, 0.4781, 0.9626, 0.5438000000000001, 0.9788, 0.6167900000000001, 0.9888, 0.70438, 0.9949, 0.8211700000000001, 0.9982, 1, 1]], [[0, 0, 0]], [[0, 0, 772.2521946132229, 0, 0, 0, 0, [[0, 1, [1, [0, 0]], [1, [0, 1]], 1, 0]]]]],
"wireHex": "4c4d5000010000000000000002000100010000011c00000000000000000000000000000000008a86f8e3d6e55d3f40048f4d0551713f9f3c2cd49ae68d3f2b8716d9cef7a33f1ae25817b7d1a03fbf7d1d386744b93fd4264eee7728aa3f51da1b7c6132c53f0a0c59ddea39b93f931804560e2dd63f990d32c9c859c03fee5a423ee8d9dc3f63d174763238c23f1ac05b2041f1df3f9f71e1404816c43f068195438b6ce13f41481630815bc73fd0d556ec2fbbe33f0a0c59ddea39c93f7aa52c431cebe43fc880ecf5ee8fcb3f302aa913d044e63f84f57f0ef3e5cd3f0a68226c787ae73f59a31ea2d11dd03f8fc2f5285c8fe83fb85d68aed348d13fa3923a014d84e93f8f705af0a2afd23ff697dd938785ea3fed2aa4fca4dad33f736891ed7c3feb3fc53d963e7441d53fe25817b7d100ec3fd63e1d8f19a8d63f68226c787aa5ec3fad510fd1e80ed83f6f8104c58f31ed3f84640113b875d93f9d8026c286a7ed3fd4cf9b8a5418db3f431cebe2361aee3f29ed0dbe3099de3faf25e4839ecdee3fd812f241cf66e13fc364aa605452ef3fb1fecf61bebce33f151dc9e53fa4ef3f7c9bfeec478ae63ff54a598638d6ef3f2d9ace4e0647ea3f1ac05b2041f1ef3f000000000000f03f000000000000f03f00000000000000000000000000000028009c7e04228840000000000000000000000000000001000000000000000000000000000000f03f010000000000000000000100000000000000f03f010000"
"program": [1, 0, [], [0, [1, 0, 0, 0.001388888888888889, 0.004328530266913981, 0.01111, 0.0401, 0.025, 0.1015, 0.03889, 0.1702, 0.075, 0.3553, 0.08889, 0.4226, 0.1, 0.4736, 0.11111, 0.5216, 0.12222, 0.5666, 0.13333, 0.6083, 0.14444, 0.6469, 0.15833, 0.6908, 0.175, 0.7373, 0.18889, 0.7714, 0.20556000000000002, 0.8071, 0.22222, 0.8378, 0.23889, 0.864, 0.25556, 0.8862, 0.275, 0.9079, 0.29444, 0.9257, 0.31389, 0.9401, 0.33332999999999996, 0.9519, 0.35556, 0.9627, 0.40556, 0.9791, 0.46389, 0.9895, 0.5305599999999999, 0.9953, 0.61944, 0.9984, 1, 1]], [[0, 0, 0]], [[0, 0, 1038.8472640593557, 0, 0, 0, 0, [[0, 1, [1, [0, 0]], [1, [0, 1]], 1, 0]]]]],
"wireHex": "4c4d5000010000000000000002000100010000011d0000000000000000000000000000000000176cc1166cc1563fb86ffafecaba713f1bbb44f5d6c0863f9ca223b9fc87a43f9a9999999999993f96438b6ce7fbb93fd36a48dc63e9a33fd3bce3141dc9c53f333333333333b33f053411363cbdd63f3602f1ba7ec1b63fce88d2dee00bdb3f9a9999999999b93facadd85f764fde3ffd304278b471bc3fc5feb27bf2b0e03f60c8ea56cf49bf3f363cbd529621e23fe2afc91af510c13f8104c58f3177e33f94fb1d8a027dc23f6c09f9a067b3e43f15e3fc4d2844c43f2575029a081be63f666666666666c63fd5e76a2bf697e73fe84d452a8c2dc83f014d840d4fafe83f3ad1ae42ca4fca3f4f401361c3d3e93ffd304278b471cc3f73d712f241cfea3f4eb4ab90f293ce3fd9cef753e3a5eb3fd09b8a54185bd03febe2361ac05bec3f9a9999999999d13f933a014d840ded3f6397a8de1ad8d23fc8073d9b559fed3ff4a62215c616d43ff7065f984c15ee3fbda4315a4755d53f2eff21fdf675ee3f3602f1ba7ec1d63f917efb3a70ceee3f693524eeb1f4d93f696ff085c954ef3f8e40bcae5fb0dd3f448b6ce7fba9ef3fb41a12f758fae03f7daeb6627fd9ef3f18b2bad573d2e33fde718a8ee4f2ef3f000000000000f03f000000000000f03f00000000000000000000000000000009883099633b9040000000000000000000000000000001000000000000000000000000000000f03f010000000000000000000100000000000000f03f010000"
}
],
"invalid": [
Expand Down
3 changes: 3 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,6 @@
| `LM167` | surface input | Ширина поверхности не является положительным конечным CSS px; либо budget точности сопряжения не положителен/не конечен | Передать положительные конечные px-endpoints bounded viewport и положительный конечный coupling budget | active |
| `LM168` | surface controls | play/pause/seek вызваны на one-shot surface-переходе: движение сертифицировано парой CSS-анимаций псевдодерева без адресата управления | Использовать контракт поверхности: committed/ready/finished/cancel; stop эквивалентен cancel | active |
| `LM169` | spring easing | у пружины не существует финитной C¹-проекции на [0,1] в допуск: затухания нет (damping = 0) либо горизонт допуска недостижим при экстремальном ζ | Задать damping > 0 и умеренное ζ либо использовать tween с явной длительностью | active |
| `LM170` | reserved | Код зарезервирован параллельной spring-работой | Не использовать вне назначенного контракта | reserved |
| `LM171` | reserved | Код зарезервирован параллельной spring-работой | Не использовать вне назначенного контракта | reserved |
| `LM172` | compositor value error | Некорректный абсолютный бюджет ошибки реконструкции | Передать положительный конечный maxValueError в единицах numeric from/to | active |
70 changes: 31 additions & 39 deletions src/compositor/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function lookupSpringLinearCache<T>(
const hash = hash5(a, b, c, d, e);
const node = cache._map.get(hash);
// Сверка исходных чисел отсекает коллизию хеша (промах, не чужой план).
if (node !== undefined && node.a === a && node.b === b && node.c === c && node.d === d && node.e === e) {
if (node && node.a === a && node.b === b && node.c === c && node.d === d && node.e === e) {
touch(cache, node);
return node._value;
}
Expand All @@ -120,49 +120,41 @@ export function storeSpringLinearCache<T>(
value: T,
): void {
const hash = hash5(a, b, c, d, e);
const existing = cache._map.get(hash);
if (existing !== undefined) {
let node = cache._map.get(hash);
if (node !== undefined) {
// Тот же хеш: либо повторный store того же ключа, либо коллизия — в обоих
// случаях перезаписываем узел на месте (реассайн, без аллокации).
existing.a = a;
existing.b = b;
existing.c = c;
existing.d = d;
existing.e = e;
existing._value = value;
touch(cache, existing);
return;
}

let node: CacheNode<T>;
if (cache._map.size >= cache._capacity) {
node = cache._tail!;
cache._map.delete(node._hash);
touch(cache, node);
node._hash = hash;
node.a = a;
node.b = b;
node.c = c;
node.d = d;
node.e = e;
node._value = value;
} else {
node = {
_hash: hash,
a,
b,
c,
d,
e,
_value: value,
_prev: undefined,
_next: cache._head,
};
if (cache._head) cache._head._prev = node;
else cache._tail = node;
cache._head = node;
if (cache._map.size >= cache._capacity) {
node = cache._tail!;
cache._map.delete(node._hash);
touch(cache, node);
node._hash = hash;
} else {
node = {
_hash: hash,
a,
b,
c,
d,
e,
_value: value,
_prev: undefined,
_next: cache._head,
};
if (cache._head) cache._head._prev = node;
else cache._tail = node;
cache._head = node;
}
cache._map.set(hash, node);
}
cache._map.set(hash, node);
node.a = a;
node.b = b;
node.c = c;
node.d = d;
node.e = e;
node._value = value;
}

// Холодный inspection/reset shell вынесен из class prototype: consumer-путь
Expand Down
19 changes: 12 additions & 7 deletions src/compositor/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export interface CompositorPlanOptions {
readonly v0?: number;
/** Толерантность (ед. прогресса). По умолчанию DEFAULT_TOLERANCE. */
readonly tolerance?: number;
/** Макс. ошибка реконструкции в единицах numeric from/to до format. */
readonly maxValueError?: number;
/** Fill. По умолчанию 'both'. */
readonly fill?: 'none' | 'forwards' | 'backwards' | 'both';
/** Composite. По умолчанию 'replace'. */
Expand Down Expand Up @@ -235,8 +237,12 @@ export function compileSpringPlan(options: CompositorPlanOptions): CompositorPla
validateFinite(options.to);
const v0 = options.v0 ?? 0;
validateFinite(v0);
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
let tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
validateTolerance(tolerance);
const maxValueError = options.maxValueError;
if (maxValueError !== undefined && !(maxValueError > 0 && maxValueError < 1 / 0)) throw new MotionParamError('LM172');
const span = Math.abs(options.to - options.from);
if (maxValueError && span) tolerance = Math.min(tolerance, maxValueError / span);

// Публичная диагностика — свежий снимок защищённых сериализованных остановок:
// это реально исполняемая браузером кривая, без второго источника истины.
Expand Down Expand Up @@ -481,17 +487,18 @@ export class CompositorSpring {
if (typeof opts.property !== 'string' || opts.property.length === 0) {
throw new MotionParamError('LM010');
}
validateFinite(opts.from);
validateFinite(opts.to);
if (opts.tolerance !== undefined) validateTolerance(opts.tolerance);
validateFinite(this._from = opts.from);
validateFinite(this._to = opts.to);
const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
validateTolerance(tolerance);
const delay = opts.delay ?? 0;
if (!Number.isFinite(delay) || delay < 0) {
throw new MotionParamError('LM013');
}

this._spring = opts.spring;
this._property = opts.property;
this._tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
this._tolerance = tolerance;
this._fill = opts.fill ?? 'both';
this._composite = opts.composite ?? 'replace';
this._format = opts.format ?? Number;
Expand All @@ -501,8 +508,6 @@ export class CompositorSpring {
this._delay = delay;
this._setTimer = opts.setTimer ?? defaultSetTimer;
this._now = opts.now ?? defaultNow;
this._from = opts.from;
this._to = opts.to;
// Детекция тира — единственное обращение к среде в конструкторе (SSR-safe),
// один раз. matchMedia (reduce) имеет высший precedence над WAAPI/linear().
this._tier = resolveCompositorTierCodeFromInputs(
Expand Down
13 changes: 6 additions & 7 deletions src/compositor/curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,11 @@ function emitArtifact(
const percent = i === 1 || percentDigits > 100
? String(node.percent)
: roundShortest(node.percent, percentDigits);
out += progress + ' ' + percent + '%';
out += (i === 0 ? '' : ', ') + progress + ' ' + percent + '%';
// Number(token) моделирует CSS parser один раз на cold compile. TypedArray
// не совпадает по identity с caller-owned raw nodes и не выходит host-коду.
samples[i * 2] = Number(percent);
samples[i * 2 + 1] = Number(progress);
if (i < nodes.length - 1) out += ', ';
samples[i * 2] = +percent;
samples[i * 2 + 1] = +progress;
}
return [out + ')', samples, durationMs];
}
Expand All @@ -148,7 +147,7 @@ export function compileSpringExecutionArtifactTupleUnchecked(
prebuiltNodes,
prebuiltDurationMs,
);
if (artifact === undefined) {
if (!artifact) {
// Ошибочный public compile остаётся fail-fast; production preflight читает
// undefined и выбирает live до смены владельца.
assertSpringCurveBudget(spring, v0, tolerance);
Expand Down Expand Up @@ -179,12 +178,12 @@ export function tryCompileSpringExecutionArtifactTupleUnchecked(
v0,
tolerance,
);
if (hit !== undefined) return hit;
if (hit) return hit;
let nodes = prebuiltNodes;
let durationMs = prebuiltDurationMs;
if (nodes === undefined) {
const build = tryBuildSpringNodes(spring, v0, tolerance);
if (build === undefined) return undefined;
if (!build) return;
nodes = build[0];
durationMs = build[1] * 1000;
}
Expand Down
Loading
Loading