std.rect — Achsenparallele Rechtecke

Zurück zur Unit-Übersicht

Rechtecke als Paar aus zwei Eckpunkten: min (links oben) und max (rechts unten), beide vom Typ Vec2 aus std.vector. Alle Rechtecke sind achsenparallel — es gibt keine Drehung. Gerechnet wird ganzzahlig.

Einsatzbereiche: Layoutflächen, Sichtbereiche, Kollisionsprüfung, umschließende Rechtecke (Bounding Boxes).

Autor: Andreas Röne
Copyright: 2024–2025 Andreas Röne
Quelle: std/rect.lyx

<WRAP alert> Drei Punkte weichen von dem ab, was die Namen versprechen:

  • RectContains und RectContainsInclusive verhalten sich genau umgekehrt zu ihrer Benennung (#1479).
  • RectCorners liefert vier Nullvektoren statt der Ecken — die Funktion füllt ihr Array nie (#1480).
  • RectDistanceToPoint misst die Manhattan-Distanz, nicht den geometrischen Abstand (#1480).

Alles Übrige ist geprüft und in Ordnung. Einzelheiten unter Was nicht stimmt. </WRAP>

Import

import std.rect;

Zieht std.vector mit — Vec2 wird für Eckpunkte und Größen gebraucht.


Der Typ Rect

Feld Typ Bedeutung
min Vec2 Ecke mit den kleineren Koordinaten
max Vec2 Ecke mit den größeren Koordinaten

Die Benennung „oben links„ und „unten rechts“ gilt für Bildschirmkoordinaten, bei denen y nach unten wächst. In einem mathematischen Koordinatensystem liegt min entsprechend unten links — die Rechnungen sind davon unberührt.

Die Unit setzt min ⇐ max voraus, prüft es aber nur in RectIsValid. Ein Rechteck mit vertauschten Ecken entsteht etwa als Ergebnis von RectIntersect bei disjunkten Eingaben; Breite und Fläche sind dann negativ. RectFromPoints sortiert die Ecken selbst und ist deshalb der sichere Konstruktor, wenn die Reihenfolge unbekannt ist.


Funktionen

Erzeugen

Signatur Beschreibung
RectNew(min, max: Vec2): Rect Direkt aus zwei Ecken — ohne Sortierung
RectFromPoints(p1, p2: Vec2): Rect Aus zwei beliebigen Punkten, sortiert die Ecken
RectFromXYWH(x, y, w, h: int64): Rect Aus Position und Größe
RectFromCenterSize(center, size: Vec2): Rect Um einen Mittelpunkt herum
RectEmpty(): Rect Alles null — gilt als ungültig und leer

RectFromCenterSize halbiert die Größe ganzzahlig: bei ungerader Breite ist das Ergebnis um eins schmaler als angegeben.

Maße

Signatur Beschreibung
RectWidth(r: Rect): int64 max.x − min.x
RectHeight(r: Rect): int64 max.y − min.y
RectSize(r: Rect): Vec2 Breite und Höhe als Vektor
RectCenter(r: Rect): Vec2 Mittelpunkt, ganzzahlig gerundet
RectArea(r: Rect): int64 Breite mal Höhe
RectLeft · RectRight · RectTop · RectBottom Einzelne Kantenkoordinate
RectTopLeft · RectTopRight · RectBottomLeft · RectBottomRight Einzelne Ecke als Vec2

Prüfen

Signatur Beschreibung Status
RectIsValid(r: Rect): bool max echt größer min in beiden Achsen ok
RectIsEmpty(r: Rect): bool Breite oder Höhe ist 0 oder negativ ok
RectEqual(a, b: Rect): bool Beide Ecken gleich ok
RectContains(r: Rect, p: Vec2): bool Punkt im Rechteck, beide Ränder eingeschlossen Name irreführend (#1479)
RectContainsInclusive(r: Rect, p: Vec2): bool Punkt im Rechteck, max-Rand ausgeschlossen Name irreführend (#1479)
RectIntersects(a, b: Rect): bool Ob sich zwei Rechtecke überlappen ok

Ein Rechteck der Breite 0 gilt als leer und ungültig — die beiden Prüfungen sind exakt gegenläufig.

Verändern

Signatur Beschreibung
RectInflate(r: Rect, amount: int64): Rect Nach allen Seiten vergrößern
RectDeflate(r: Rect, amount: int64): Rect Nach allen Seiten verkleinern
RectExpand(r: Rect, p: Vec2): Rect So erweitern, dass p enthalten ist
RectUnion(a, b: Rect): Rect Kleinstes Rechteck um beide
RectIntersect(a, b: Rect): Rect Überlappungsfläche
RectClampPoint(r: Rect, p: Vec2): Vec2 Punkt in das Rechteck hineinziehen
RectClamp(r, bounds: Rect): Rect Rechteck auf einen Rahmen beschneiden — identisch zu RectIntersect

RectDeflate kann ein Rechteck ungültig machen, wenn der Betrag größer als die halbe Kantenlänge ist. Das Ergebnis sollte danach mit RectIsValid geprüft werden. RectIntersect liefert bei disjunkten Rechtecken ein Ergebnis mit vertauschten Ecken; genau darauf stützt sich RectIntersects.

Sonstiges

Signatur Beschreibung Status
RectDistanceToPoint(r: Rect, p: Vec2): int64 Abstand eines Punkts zum Rechteck; 0 wenn innen Manhattan (#1480)
RectCorners(r: Rect): [4]Vec2 Alle vier Ecken auf einmal liefert Nullvektoren (#1480)

Beispiel

import std.rect;
import std.vector;

fn zeigR(name: pchar, r: Rect): void {
    PrintLn(name, " = min(", IntToStr(r.min.x), ",", IntToStr(r.min.y),
            ")  max(", IntToStr(r.max.x), ",", IntToStr(r.max.y), ")");
}

fn main(): int64 {
    // Ein Fenster: Position (10,20), 100 breit, 50 hoch
    var fenster: Rect := RectFromXYWH(10, 20, 100, 50);
    zeigR("Fenster ", fenster);
    PrintLn("Breite ", IntToStr(RectWidth(fenster)), ", Hoehe ", IntToStr(RectHeight(fenster)),
            ", Flaeche ", IntToStr(RectArea(fenster)));
    var m: Vec2 := RectCenter(fenster);
    PrintLn("Mittelpunkt = (", IntToStr(m.x), ", ", IntToStr(m.y), ")");

    // Rand abziehen und wieder aufschlagen
    zeigR("Innenraum", RectDeflate(fenster, 5));
    zeigR("mit Rahmen", RectInflate(fenster, 5));

    // Ueberschneidung zweier Bereiche
    var a: Rect := RectFromXYWH(0, 0, 100, 100);
    var b: Rect := RectFromXYWH(50, 50, 100, 100);
    PrintLn("-- zwei Bereiche:");
    PrintLn("ueberschneiden sich? ", IntToStr(RectIntersects(a, b) as int64));
    zeigR("Schnittflaeche", RectIntersect(a, b));
    zeigR("Umschliessend ", RectUnion(a, b));

    // Punkt einpassen
    var p: Vec2 := RectClampPoint(a, Vec2New(150, -20));
    PrintLn("ClampPoint((150,-20)) = (", IntToStr(p.x), ", ", IntToStr(p.y), ")");

    // Bounding-Box ueber mehrere Punkte aufbauen
    var box: Rect := RectFromPoints(Vec2New(40, 10), Vec2New(40, 10));
    box := RectExpand(box, Vec2New(-5, 60));
    box := RectExpand(box, Vec2New(120, 25));
    zeigR("Bounding-Box  ", box);
    return 0;
}

Ausgabe:

Fenster  = min(10,20)  max(110,70)
Breite 100, Hoehe 50, Flaeche 5000
Mittelpunkt = (60, 45)
Innenraum = min(15,25)  max(105,65)
mit Rahmen = min(5,15)  max(115,75)
-- zwei Bereiche:
ueberschneiden sich? 1
Schnittflaeche = min(50,50)  max(100,100)
Umschliessend  = min(0,0)  max(150,150)
ClampPoint((150,-20)) = (100, 0)
Bounding-Box   = min(-5,10)  max(120,60)

Das Muster am Ende — mit einem Punkt beginnen und per RectExpand erweitern — ist der übliche Weg zu einer Bounding Box über eine Punktmenge. Wichtig ist der Start mit RectFromPoints(p, p) statt mit RectEmpty(): Letzteres läge bei (0,0) und würde den Ursprung fälschlich einschließen.


Was nicht stimmt

Geprüft mit lyxc 1.0.21A.

Contains und ContainsInclusive sind vertauscht (#1479)

Für das Rechteck 10…110 × 20…70 und den Eckpunkt max:

Contains(110,70)          = 1      der max-Rand gilt als enthalten
ContainsInclusive(110,70) = 0      ausgerechnet die "inklusive" Variante schliesst ihn aus

RectContains vergleicht mit und schließt damit beide Ränder ein; RectContainsInclusive vergleicht mit < und lässt den max-Rand außen vor.

Die halboffene Variante — also RectContainsInclusive — ist für Pixelraster und Kachelgitter die richtige: ein Punkt auf der gemeinsamen Kante zweier angrenzender Rechtecke gehört dann zu genau einem von beiden. Bei der Auswahl nach dem Namen bekommt man derzeit das Gegenteil und zählt Treffer doppelt.

RectCorners liefert keine Ecken (#1480)

var ecken: [4]Vec2 := RectCorners(RectFromXYWH(0, 0, 100, 100));

Ecke 0 = (0, 0)
  Ecke 1 = (0, 0)
  Ecke 2 = (0, 0)
  Ecke 3 = (0, 0)

Die Funktion legt das Array an und gibt es zurück, ohne es zu füllen — der Parameter r wird nicht verwendet. Dass hier Nullen erscheinen, ist Zufall des Stackinhalts.

Ersatz über die vier Einzelfunktionen, die korrekt arbeiten:

var ecken: [4]Vec2;
ecken[0] := RectTopLeft(r);
ecken[1] := RectTopRight(r);
ecken[2] := RectBottomRight(r);
ecken[3] := RectBottomLeft(r);

RectDistanceToPoint misst Manhattan (#1480)

DistanceToPoint(Rect 0..100, Punkt (130,140)) = 70

Der Punkt liegt 30 rechts und 40 unterhalb der Ecke (100,100); der geometrische Abstand ist 50. Geliefert wird die Summe der beiden Abstände. Liegt der Punkt achsenparallel außerhalb, stimmt das Ergebnis zufällig — (130, 0) ergibt korrekt 30. Bei diagonaler Lage ist es zu groß, was eine Suche nach dem nächstgelegenen Rechteck verfälschen kann.

Ersatz:

import std.math;
// dx, dy wie in der Unit bestimmen, dann:
var abstand: int64 := IntSqrt(dx * dx + dy * dy);


Verwandte Units