waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app src shared fensterlage.ts
| 1 | /** |
| 2 | * Größe und Lage des Hauptfensters – reine Logik, ohne Electron. |
| 3 | * |
| 4 | * **Wozu.** Das Fenster startete bis 0.22.0 immer mit derselben Größe an der |
| 5 | * Systemposition. Wer maximiert arbeitet oder es schmal neben eine |
| 6 | * Bildschirmlupe oder ein zweites Programm stellt, richtete das bei jedem |
| 7 | * Start neu ein — und das sind gerade die Nutzer, für die diese Anwendung |
| 8 | * gebaut ist. |
| 9 | * |
| 10 | * **Warum die Prüfung hier liegt und nicht bei der Fenstererzeugung.** An ihr |
| 11 | * hängt der einzige gefährliche Fall: eine gespeicherte Lage auf einem |
| 12 | * Bildschirm, den es nicht mehr gibt. Das Fenster ginge dann außerhalb jeder |
| 13 | * sichtbaren Fläche auf — vorhanden, aber unerreichbar, und ohne Fenster gibt |
| 14 | * es auch keinen Weg, die Einstellung zurückzusetzen. Als reine Funktion |
| 15 | * lässt sich das prüfen, ohne einen zweiten Monitor abzustecken. |
| 16 | */ |
| 17 | |
| 18 | /** Fenstergröße und -lage, wie sie gespeichert wird. */ |
| 19 | export interface Fensterlage { |
| 20 | readonly breite: number; |
| 21 | readonly hoehe: number; |
| 22 | readonly x?: number; |
| 23 | readonly y?: number; |
| 24 | readonly maximiert?: boolean; |
| 25 | } |
| 26 | |
| 27 | /** Ein Rechteck einer verfügbaren Bildschirmfläche. */ |
| 28 | export interface Flaeche { |
| 29 | readonly x: number; |
| 30 | readonly y: number; |
| 31 | readonly width: number; |
| 32 | readonly height: number; |
| 33 | } |
| 34 | |
| 35 | /** Vorgabe, wenn nichts gespeichert ist oder das Gespeicherte nicht taugt. */ |
| 36 | export const FENSTER_VORGABE = Object.freeze({ breite: 1180, hoehe: 820 }); |
| 37 | |
| 38 | /** Untergrenzen; dieselben wie `minWidth`/`minHeight` am Fenster. */ |
| 39 | export const FENSTER_MINDESTBREITE = 360; |
| 40 | export const FENSTER_MINDESTHOEHE = 560; |
| 41 | |
| 42 | /** So viel vom Fenster muss auf einer Fläche liegen, damit es greifbar ist. */ |
| 43 | const SICHTBAR_MINDESTENS = 80; |
| 44 | |
| 45 | function zahl(wert: unknown): number | null { |
| 46 | return typeof wert === 'number' && Number.isFinite(wert) ? Math.round(wert) : null; |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Nimmt eine gespeicherte Lage entgegen und gibt zurück, was davon taugt. |
| 51 | * |
| 52 | * `null` heißt: nichts Brauchbares, es gilt die Vorgabe. |
| 53 | */ |
| 54 | export function fensterlageBereinigen(wert: unknown): Fensterlage | null { |
| 55 | if (typeof wert !== 'object' || wert === null) { |
| 56 | return null; |
| 57 | } |
| 58 | const roh = wert as Record<string, unknown>; |
| 59 | |
| 60 | const breite = zahl(roh['breite']); |
| 61 | const hoehe = zahl(roh['hoehe']); |
| 62 | if (breite === null || hoehe === null) { |
| 63 | return null; |
| 64 | } |
| 65 | |
| 66 | const x = zahl(roh['x']); |
| 67 | const y = zahl(roh['y']); |
| 68 | |
| 69 | return { |
| 70 | /* Nach unten gekappt, nicht nach oben: Ein zu großes Fenster schiebt das |
| 71 | Betriebssystem selbst zurecht, ein zu kleines wäre unbedienbar. */ |
| 72 | breite: Math.max(FENSTER_MINDESTBREITE, breite), |
| 73 | hoehe: Math.max(FENSTER_MINDESTHOEHE, hoehe), |
| 74 | ...(x === null ? {} : { x }), |
| 75 | ...(y === null ? {} : { y }), |
| 76 | ...(roh['maximiert'] === true ? { maximiert: true } : {}), |
| 77 | }; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Ob die gespeicherte Lage auf einer der vorhandenen Flächen liegt. |
| 82 | * |
| 83 | * Verlangt wird kein vollständiges Enthaltensein: Ein Fenster, das ein Stück |
| 84 | * über den Rand ragt, ist gewollt und alltäglich. Verlangt wird, dass ein |
| 85 | * greifbarer Teil sichtbar bleibt — sonst ließe es sich nicht einmal mehr |
| 86 | * anfassen, um es zurückzuholen. |
| 87 | */ |
| 88 | export function lageIstErreichbar(lage: Fensterlage, flaechen: readonly Flaeche[]): boolean { |
| 89 | const { x, y } = lage; |
| 90 | if (x === undefined || y === undefined) { |
| 91 | /* Ohne Position entscheidet das Betriebssystem – das ist immer gültig. */ |
| 92 | return true; |
| 93 | } |
| 94 | |
| 95 | return flaechen.some((flaeche) => { |
| 96 | const ueberlappungX = |
| 97 | Math.min(x + lage.breite, flaeche.x + flaeche.width) - Math.max(x, flaeche.x); |
| 98 | const ueberlappungY = |
| 99 | Math.min(y + lage.hoehe, flaeche.y + flaeche.height) - Math.max(y, flaeche.y); |
| 100 | return ueberlappungX >= SICHTBAR_MINDESTENS && ueberlappungY >= SICHTBAR_MINDESTENS; |
| 101 | }); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Die Lage, mit der das Fenster aufgehen soll. |
| 106 | * |
| 107 | * Fällt auf die Vorgabe zurück, wenn nichts gespeichert ist oder die |
| 108 | * gespeicherte Lage auf keiner vorhandenen Fläche mehr liegt. |
| 109 | */ |
| 110 | export function startlage(gespeichert: unknown, flaechen: readonly Flaeche[]): Fensterlage { |
| 111 | const lage = fensterlageBereinigen(gespeichert); |
| 112 | if (lage === null || !lageIstErreichbar(lage, flaechen)) { |
| 113 | return { ...FENSTER_VORGABE }; |
| 114 | } |
| 115 | return lage; |
| 116 | } |