Snap-to-grid for moving/resizing canvas objects, plus a per-sheet grid size

Sheet.gridSize (4-200px, default 24, configurable in Settings -> Blatt)
now doubles as both the visible dot-grid's spacing and the snap
increment while dragging to move any canvas object, or free/viewport-
resizing a chart/text/button/fixed-viewport table. An "Automatisch"
table's resize is left unsnapped on top of this -- it already
discretizes to row/col boundaries by construction.

Also fixes the visible grid not actually lining up with real snap
positions once zoomed: the dot pattern's background-size now scales
with zoom like the objects themselves already did, instead of staying a
fixed screen-pixel size regardless of zoom level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-09 23:33:39 +02:00
co-authored by Claude Sonnet 5
parent 5f75b9c298
commit d0500fcaf5
8 changed files with 101 additions and 14 deletions
+20 -1
View File
@@ -297,6 +297,21 @@ describe('per-sheet appearance, zoom & CSV default (saved in the file)', () => {
void b;
});
it('grid size (snap increment) is per-sheet, clamped and rounded', () => {
const s = fresh();
const a = s.doc.sheets[0].id;
const b = s.doc.sheets[1].id;
expect(s.doc.sheets[0].gridSize).toBeUndefined(); // unset = DEFAULT_GRID_SIZE at use-sites
s.setSheetGridSize(a, 40.6);
expect(s.doc.sheets[0].gridSize).toBe(41); // rounded
expect(s.doc.sheets[1].gridSize).toBeUndefined(); // untouched
s.setSheetGridSize(a, 1); // below MIN_GRID_SIZE
expect(s.doc.sheets[0].gridSize).toBe(4);
s.setSheetGridSize(a, 9999); // above MAX_GRID_SIZE
expect(s.doc.sheets[0].gridSize).toBe(200);
void b;
});
it('zoom is stored per sheet; the active-sheet zoom follows the tab', () => {
const s = fresh();
expect(s.activeZoom.value).toBe(1);
@@ -310,26 +325,30 @@ describe('per-sheet appearance, zoom & CSV default (saved in the file)', () => {
expect(s.activeZoom.value).toBe(1.5); // sheet 1 kept its own
});
it('appearance + zoom + csvDelimiter survive a JSON round-trip; bad values dropped', () => {
it('appearance + zoom + gridSize + csvDelimiter survive a JSON round-trip; bad values dropped', () => {
const s = fresh();
const a = s.doc.sheets[0].id;
s.setSheetBackground(a, '#abcdef');
s.setSheetForeground(a, '#012');
s.setSheetShowGrid(a, false);
s.setSheetZoom(a, 1.25);
s.setSheetGridSize(a, 32);
s.setCsvDelimiter(';');
const before = s.snapshot();
s.replaceDocument(parseDocument(serializeDocument(before)));
expect(s.snapshot()).toEqual(before);
expect(s.doc.csvDelimiter).toBe(';');
expect(s.doc.sheets[0].gridSize).toBe(32);
const doc = JSON.parse(serializeDocument(s.snapshot()));
doc.sheets[0].background = 'not-a-color';
doc.sheets[0].zoom = 99;
doc.sheets[0].gridSize = 'huge';
doc.csvDelimiter = 'x';
s.replaceDocument(parseDocument(JSON.stringify(doc)));
expect(s.doc.sheets[0].background).toBeUndefined();
expect(s.doc.sheets[0].zoom).toBe(2); // clamped, not dropped
expect(s.doc.sheets[0].gridSize).toBeUndefined(); // not a number -> dropped
expect(s.doc.csvDelimiter).toBe(',');
});
});