diff --git a/README.md b/README.md index 21d96c1..42ee4c0 100644 --- a/README.md +++ b/README.md @@ -632,8 +632,8 @@ wichtigen Punkte: - Textfeld → Name, Text, Schriftgröße, Fett/Kursiv, Ausrichtung, **Textfarbe**, **Hintergrund**, Größe, Position. - **Blatt**: Name, Zahlenformat, **Hintergrundfarbe**, **Schriftfarbe**, - **Raster ein-/ausblenden** (`↺` setzt eine Farbe auf den Theme-Wert zurück), - neues Blatt, Blatt löschen. + **Raster ein-/ausblenden**, **Rasterweite** (`↺` setzt eine Farbe auf den + Theme-Wert zurück), neues Blatt, Blatt löschen. - **Ansicht**: Zoom-Regler (**pro Blatt** gespeichert). - **Dokument**: Erscheinungsbild (Auto/Hell/Dunkel), Zahlenformat (Ländereinstellung), CSV-Standard-Trennzeichen, „Auf Beispiel zurücksetzen". @@ -646,6 +646,17 @@ das **Punkt-Raster** ein- oder ausblenden. Die Farben überschreiben die Theme-Variablen `--bg` / `--text` nur für dieses Blatt; ohne Wert gilt das Theme. Der Canvas-**Zoom** wird ebenfalls pro Blatt gemerkt. +Die **Rasterweite** (`Sheet.gridSize`, 4–200 px, Standard 24) ist ebenfalls +pro Blatt einstellbar (Einstellungen → Blatt) und dient **doppelt**: sie +bestimmt sowohl den Abstand des sichtbaren Punkt-Rasters (skaliert mit dem +Zoom, damit die Punkte immer exakt dort liegen, wo ein Objekt tatsächlich +einrastet) als auch die **Schrittweite beim Einrasten** (Snapping), wenn ein +Tabellen-, Diagramm-, Text- oder Button-Objekt per Maus **verschoben** oder +(bei „Feste Größe"/Diagramm/Text/Button) **frei skaliert** wird. Eine +„Automatisch"-Tabelle rastet beim Skalieren stattdessen bereits durch ihre +Zeilen-/Spaltengrenzen selbst ein und wird nicht zusätzlich pixelweise +gerastert. + Alle diese Einstellungen sind Teil des Dokuments (`DocumentState` / `Sheet`) und werden damit in **localStorage gespeichert und in `.json` exportiert** – Round-Trip verlustfrei, ungültige Werte werden beim Import diff --git a/src/components/CanvasObject.vue b/src/components/CanvasObject.vue index e197368..7bb53c4 100644 --- a/src/components/CanvasObject.vue +++ b/src/components/CanvasObject.vue @@ -17,11 +17,14 @@ * - name is editable via double-click, delete via the × button. * * All pointer deltas are divided by `zoom` so dragging tracks the cursor at any - * zoom level. + * zoom level. Moving and free/viewport resizing both snap to the object's own + * sheet's grid spacing (`Sheet.gridSize`, "Raster" in the Settings panel) -- + * an "Automatisch" table's resize instead already snaps to row/col + * boundaries by construction, so it isn't additionally pixel-snapped. */ import { computed, nextTick, ref } from 'vue'; import type { SheetObject, TableObject } from '../store/types'; -import { DEFAULT_ROW_H } from '../store/types'; +import { DEFAULT_ROW_H, DEFAULT_GRID_SIZE } from '../store/types'; import { useDocument } from '../store/document'; import TableGrid from './TableGrid.vue'; import ChartView from './ChartView.vue'; @@ -47,6 +50,16 @@ function commitRename() { renaming.value = false; } +// ---- move/resize snap-to-grid -------------------------------------- +/** This object's own sheet's grid spacing -- also the snap increment while + * dragging to move or resize it (SettingsPanel's "Raster" field). */ +const gridSize = computed(() => { + const sheetId = store.sheetIdOfObject(props.object.id); + const sheet = sheetId ? store.sheetById(sheetId) : undefined; + return sheet?.gridSize ?? DEFAULT_GRID_SIZE; +}); +const snap = (v: number) => Math.round(v / gridSize.value) * gridSize.value; + // ---- drag to move ------------------------------------------------- let drag: { px: number; py: number; ox: number; oy: number } | null = null; function onMoveDown(e: MouseEvent) { @@ -61,7 +74,7 @@ function onMoveMove(e: MouseEvent) { if (!drag) return; const dx = (e.clientX - drag.px) / props.zoom; const dy = (e.clientY - drag.py) / props.zoom; - store.moveObject(props.object.id, drag.ox + dx, drag.oy + dy); + store.moveObject(props.object.id, snap(drag.ox + dx), snap(drag.oy + dy)); } function onMoveUp() { drag = null; @@ -174,14 +187,14 @@ function onResizeMove(e: MouseEvent) { const dy = (e.clientY - rs.py) / props.zoom; const o = props.object; if (resizeKind.value === 'free') { - if (o.kind === 'chart') store.resizeChart(o.id, rs.w + dx, rs.h + dy); - else if (o.kind === 'text') store.resizeText(o.id, rs.w + dx, rs.h + dy); - else if (o.kind === 'button') store.resizeButton(o.id, rs.w + dx, rs.h + dy); + if (o.kind === 'chart') store.resizeChart(o.id, snap(rs.w + dx), snap(rs.h + dy)); + else if (o.kind === 'text') store.resizeText(o.id, snap(rs.w + dx), snap(rs.h + dy)); + else if (o.kind === 'button') store.resizeButton(o.id, snap(rs.w + dx), snap(rs.h + dy)); } else if (resizeKind.value === 'viewport') { // "Feste Größe": pure pixel-viewport resize, independent of row/col // count -- cheap (setTableView never rebuilds the workbook), so a live // update on every mousemove is fine, same as it always was. - store.setTableView(o.id, { width: rs.w + dx, height: rs.h + dy }); + store.setTableView(o.id, { width: snap(rs.w + dx), height: snap(rs.h + dy) }); } else { // "Automatisch": dragging drives row/col *count* -- but that's a full // syncWorkbook() per call, too expensive for every mousemove, so only a diff --git a/src/components/SettingsPanel.vue b/src/components/SettingsPanel.vue index e68889c..9b7b30d 100644 --- a/src/components/SettingsPanel.vue +++ b/src/components/SettingsPanel.vue @@ -10,6 +10,7 @@ import { useDocument } from '../store/document'; import { useTheme } from '../composables/useTheme'; import { NUMBER_LOCALES, NUM_FORMATS } from '../store/format'; import type { CellFormat, HAlign } from '../store/types'; +import { DEFAULT_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_SIZE } from '../store/types'; const store = useDocument(); const { mode: themeMode } = useTheme(); @@ -461,6 +462,17 @@ function resetDoc() { /> Raster einblenden +