v1.60.0: harden background editing and device state

This commit is contained in:
Matysh
2026-08-07 13:02:46 +03:00
parent 29fb9deb43
commit 3028122016
47 changed files with 2566 additions and 674 deletions
+72 -1
View File
@@ -1,7 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
boxCorners, decorStrokeUnits, decorStyleOf, resizeDecorBox, snapDecorPoint,
boxCorners, decorStrokeUnits, decorStyleOf, decorStylePatch, normalizeAngle,
resizeDecorBox, resizedBoxTopLeft, snapDecorPoint, validDecorDraft,
} from '../test-build/editors/decor/geometry.js';
const close = (actual, expected, epsilon = 1e-9) =>
@@ -17,6 +18,37 @@ test('canonical physical stroke and legacy render stroke remain visually exact',
close(legacy.fillOpacity, 0.25);
});
test('decor style fallbacks and canonical patch stay inside the persisted schema', () => {
const fallback = {
color: '#112233', opacity: 0.8, widthCm: 4,
fill: false, fillColor: '#445566', fillOpacity: 0.3,
};
const style = decorStyleOf({ id: 'r', kind: 'rect', x: 0, y: 0, w: 1, h: 1,
color: 'invalid', opacity: 8, fill: true, fill_color: 'invalid' }, 5, 12, fallback);
assert.equal(style.color, fallback.color);
assert.equal(style.opacity, 1);
assert.equal(style.fillColor, fallback.color);
assert.equal(style.fillOpacity, 0.25);
const high = decorStylePatch({ ...fallback, widthCm: 1e6, opacity: -2,
fill: true, fillOpacity: 4 }, true);
assert.equal(high.width_cm, 100);
assert.equal(high.opacity, 0);
assert.equal(high.fill_opacity, 1);
const lowLine = decorStylePatch({ ...fallback, widthCm: -1 }, false);
assert.equal(lowLine.width_cm, 0.1);
assert.equal('fill' in lowLine, false);
});
test('angle normalization is stable at wrap boundaries', () => {
assert.equal(normalizeAngle(0), 0);
assert.equal(normalizeAngle(180), 180);
assert.equal(normalizeAngle(181), -179);
assert.equal(normalizeAngle(-181), 179);
assert.equal(normalizeAngle(360), 0);
assert.equal(normalizeAngle(Number.POSITIVE_INFINITY), 0);
});
test('oriented resize preserves ratio by default and Shift-style mode separates axes', () => {
const original = { x: 10, y: 20, w: 40, h: 20, angle: 30 };
const proportional = resizeDecorBox(original, 1, 1, 100, 100, true, 5, 5);
@@ -38,9 +70,48 @@ test('decor magnet may choose a point or edge but always returns a grid point',
const edge = snapDecorPoint([63, 47], geometry, 8, grid);
assert.equal(edge.kind, 'edge');
assert.deepEqual(edge.point, [60, 50]);
const empty = snapDecorPoint([14, 26], { points: [], segments: [] }, 8, grid);
assert.deepEqual(empty, { point: [10, 30], target: null, kind: 'grid' });
const tie = snapDecorPoint([20, 20], {
points: [[20, 30]], segments: [{ a: [10, 10], b: [30, 10] }],
}, 10, grid);
assert.equal(tie.kind, 'point', 'equal-distance point wins deterministically');
});
test('rotated box exposes four rotated content corners around the same centre', () => {
const corners = boxCorners({ x: 0, y: 0, w: 20, h: 10, angle: 90 });
assert.deepEqual(corners.map((p) => p.map((v) => Math.round(v))), [[15, -5], [15, 15], [5, 15], [5, -5]]);
});
test('oriented resize caller preserves the derived top-left instead of translating the fixed corner', () => {
const original = { x: 10, y: 20, w: 40, h: 20, angle: 30 };
const resized = resizeDecorBox(original, 1, 1, 97, 83, true, 5, 5);
const fixedBefore = boxCorners(original)[0];
const fixedAfter = boxCorners(resized)[0];
close(fixedAfter[0], fixedBefore[0]);
close(fixedAfter[1], fixedBefore[1]);
const grid = ([x, y]) => [Math.round(x / 5) * 5, Math.round(y / 5) * 5];
assert.deepEqual(resizedBoxTopLeft(resized, original.angle, grid), [resized.x, resized.y]);
assert.deepEqual(resizedBoxTopLeft({ x: 12.2, y: 18.1 }, 0, grid), [10, 20]);
});
test('resize cannot flip across the fixed corner and honours the common grid scale', () => {
const original = { x: 0, y: 0, w: 40, h: 20, angle: 0 };
const common = resizeDecorBox(original, 1, 1, 51, 31, true, 10, 10);
assert.deepEqual([common.w, common.h], [60, 30]);
assert.equal(common.w % 10, 0);
assert.equal(common.h % 10, 0);
const crossed = resizeDecorBox(original, 1, 1, -100, -100, false, 10, 10);
assert.deepEqual([crossed.w, crossed.h], [10, 10]);
assert.deepEqual(boxCorners(crossed)[0], boxCorners(original)[0],
'opposite corner stays fixed at the minimum-size floor');
});
test('flat rectangle and ellipse drafts are rejected while a straight line is valid', () => {
assert.equal(validDecorDraft('line', [0, 0], [20, 0], 5), true);
assert.equal(validDecorDraft('rect', [0, 0], [20, 0], 5), false);
assert.equal(validDecorDraft('ellipse', [0, 0], [0, 20], 5), false);
assert.equal(validDecorDraft('rect', [0, 0], [20, 10], 5), true);
});