====== std.circle — Kreise und Zahlenbereiche ======
→ [[lyx_-_programmiersprache:units|Zurück zur Unit-Übersicht]]
Zwei getrennte Themen in einer Unit: **Kreise** in der Ebene (Mittelpunkt als ''Vec2'' plus Radius) und **Zahlenbereiche** — ''Range'' für eine Achse, ''Range2D'' für zwei. Gerechnet wird ganzzahlig in denselben Einheiten wie die Koordinaten.
Einsatzbereiche: Umkreissuche, Kollisionsvorprüfung, Wertebereiche und Intervallprüfungen.
**Autor:** Andreas Röne\\
**Copyright:** 2024–2025 Andreas Röne\\
**Quelle:** ''std/circle.lyx''
Zwei Funktionen sind fehlerhaft:
* ''CircleArea'' liefert für jeden Radius unter 1000 den Wert **0** — Fläche und Umfang gehen von unterschiedlichen Einheiten aus (#1489).
* ''CircleUnion'' lässt den Mittelpunkt stehen, wo er war; der Ergebniskreis **deckt den zweiten Kreis nicht ab** (#1490).
Der gesamte übrige Teil — Kreisgeometrie wie ''Range''/''Range2D'' — ist geprüft und in Ordnung.
===== Import =====
import std.circle;
Zieht ''std.vector'', ''std.rect'' und ''std.math'' mit.
----
===== Kreise =====
^ Feld von ''Circle'' ^ Typ ^ Bedeutung ^
| ''center'' | ''Vec2'' | Mittelpunkt |
| ''radius'' | ''int64'' | Radius in denselben Einheiten wie die Koordinaten |
==== Erzeugen ====
^ Signatur ^ Beschreibung ^
| ''CircleNew(center: Vec2, radius: int64): Circle'' | Aus Mittelpunkt und Radius |
| ''CircleFromXYR(x, y, r: int64): Circle'' | Aus Einzelwerten |
| ''CircleFromPoints(p1, p2: Vec2): Circle'' | Kleinster Kreis, dessen Durchmesser die Strecke ''p1''–''p2'' ist |
| ''CircleUnit(): Circle'' | Mittelpunkt (0,0), Radius **1 000 000** — siehe Hinweis unten |
''CircleUnit'' geht als einzige Funktion von einem Festkomma-Radius aus (1 000 000 = 1,0). Alle übrigen Kreisfunktionen rechnen in rohen Einheiten; der „Einheitskreis" passt daher nicht zu ihnen (#1489).
==== Maße ====
^ Signatur ^ Beschreibung ^ Status ^
| ''CircleDiameter(c: Circle): int64'' | Doppelter Radius | ok |
| ''CircleCircumference(c: Circle): int64'' | Umfang, ganzzahlig abgerundet | ok |
| ''CircleArea(c: Circle): int64'' | Flächeninhalt | **immer 0** (#1489) |
| ''CircleIsEmpty(c: Circle): bool'' | Radius kleiner oder gleich 0 | ok |
==== Lagebeziehungen ====
^ Signatur ^ Beschreibung ^ Status ^
| ''CircleContainsPoint(c: Circle, p: Vec2): bool'' | Punkt im Kreis, **Rand eingeschlossen** | ok |
| ''CircleContainsCircle(c, inner: Circle): bool'' | Kreis vollständig enthalten | ok |
| ''CircleIntersectsCircle(a, b: Circle): bool'' | Überschneidung; Berührung zählt **nicht** | ok |
| ''CircleIntersectsRect(c: Circle, r: Rect): bool'' | Überschneidung mit einem Rechteck | ok |
| ''CircleDistanceToPoint(c: Circle, p: Vec2): int64'' | Abstand zum Rand, 0 wenn innen | ok |
| ''CircleDistanceToCircle(c, other: Circle): int64'' | Abstand zwischen den Rändern, 0 bei Überschneidung | ok |
| ''CircleEqual(a, b: Circle): bool'' | Mittelpunkt und Radius gleich | ok |
''CircleContainsPoint'' vergleicht die **quadrierten** Abstände und zieht keine Wurzel — dadurch ist die Prüfung exakt, ohne Rundungsfehler. Das ist der Unterschied zu ''CircleContainsCircle'' und ''CircleIntersectsCircle'', die über ''Vec2Distance'' gehen und deshalb den abgerundeten Abstand verwenden.
==== Verändern ====
^ Signatur ^ Beschreibung ^ Status ^
| ''CircleExpand(c: Circle, amount: int64): Circle'' | Radius ändern, Mittelpunkt bleibt | ok |
| ''CircleBoundingRect(c: Circle): Rect'' | Umschließendes achsenparalleles Rechteck | ok |
| ''CircleUnion(a, b: Circle): Circle'' | Kleinster Kreis um beide | **Mittelpunkt falsch** (#1490) |
''CircleBoundingRect'' ist der übliche Grobfilter: erst die Rechtecke vergleichen, dann für die verbleibenden Kandidaten den genauen Kreistest.
----
===== Zahlenbereiche =====
''Range'' beschreibt ein Intervall auf einer Achse. ''RangeNew'' **sortiert die Grenzen selbst**, ein verdrehtes Intervall kann also gar nicht entstehen.
^ Signatur ^ Beschreibung ^
| ''RangeNew(min, max: int64): Range'' | Intervall, Grenzen werden sortiert |
| ''RangeFromValue(value, delta: int64): Range'' | ''value ± delta'' |
| ''RangeEmpty(): Range'' | ''[0, 0]'' |
| ''RangeLength(r: Range): int64'' | ''max − min'' |
| ''RangeCenter(r: Range): int64'' | Mitte, ganzzahlig |
| ''RangeIsEmpty(r: Range): bool'' | ''min >= max'' |
| ''RangeContains(r: Range, value: int64): bool'' | Wert im Intervall, **Grenzen eingeschlossen** |
| ''RangeContainsExclusive(r: Range, value: int64): bool'' | ohne die Grenzen |
| ''RangeContainsRange(r, inner: Range): bool'' | Intervall vollständig enthalten |
| ''RangeIntersects(r, other: Range): bool'' | Überschneidung; reine Berührung zählt nicht |
| ''RangeIntersection(r, other: Range): Range'' | Gemeinsamer Teil |
| ''RangeUnion(r, other: Range): Range'' | Kleinstes Intervall um beide |
| ''RangeExpand(r: Range, value: int64): Range'' | So erweitern, dass ''value'' enthalten ist |
| ''RangeInflate(r: Range, amount: int64): Range'' | Nach beiden Seiten vergrößern |
**''RangeIntersection'' erkennt man den leeren Schnitt nicht an.** Weil ''RangeNew'' die Grenzen sortiert, wird aus dem rechnerisch leeren Ergebnis ein scheinbar gültiges Intervall: ''RangeIntersection([10,50], [60,80])'' liefert ''[50, 60]''. Vor dem Schnitt daher ''RangeIntersects'' abfragen.
Bei [[lyx_-_programmiersprache:units:rect|std.rect]] ist das anders gelöst — dort bleibt ''RectIntersect'' verdreht, und ''RectIsValid'' erkennt den Fall.
==== Range2D ====
''Range2D'' fasst zwei ''Range'' zusammen — für x und y.
^ Signatur ^ Beschreibung ^
| ''Range2DNew(x_range, y_range: Range): Range2D'' | Aus zwei Intervallen |
| ''Range2DFromPoints(p1, p2: Vec2): Range2D'' | Aus zwei Eckpunkten |
| ''Range2DFromRect(r: Rect): Range2D'' | Aus einem Rechteck |
| ''Range2DToRect(r2d: Range2D): Rect'' | Zurück in ein Rechteck |
| ''Range2DContains(r2d: Range2D, p: Vec2): bool'' | Punkt in beiden Achsen enthalten |
| ''Range2DIntersects(r2d, other: Range2D): bool'' | Überschneidung in beiden Achsen |
Auf die Felder wird verschachtelt zugegriffen — ''r2d.x.min'' funktioniert:
var r2: Range2D := Range2DFromPoints(Vec2New(0,0), Vec2New(100,50));
PrintLn(IntToStr(r2.x.min), " bis ", IntToStr(r2.x.max)); // 0 bis 100
''Range2D'' und ''Rect'' beschreiben dasselbe; die Umwandlungsfunktionen erlauben den Wechsel je nachdem, ob man achsenweise oder eckenweise denken will.
----
===== Beispiel =====
import std.circle;
import std.vector;
import std.rect;
fn main(): int64 {
// Ein Suchradius um einen Punkt
var umkreis: Circle := CircleFromXYR(100, 100, 50);
PrintLn("Radius ", IntToStr(umkreis.radius), ", Durchmesser ", IntToStr(CircleDiameter(umkreis)),
", Umfang ", IntToStr(CircleCircumference(umkreis)));
// Welche Punkte liegen darin?
PrintLn("(100,100) mittig = ", IntToStr(CircleContainsPoint(umkreis, Vec2New(100,100)) as int64));
PrintLn("(150,100) am Rand = ", IntToStr(CircleContainsPoint(umkreis, Vec2New(150,100)) as int64));
PrintLn("(151,100) knapp draussen = ", IntToStr(CircleContainsPoint(umkreis, Vec2New(151,100)) as int64));
// Abstand bis zum Rand
PrintLn("Abstand von (250,100) = ", IntToStr(CircleDistanceToPoint(umkreis, Vec2New(250,100))));
// Grobfilter ueber das umschliessende Rechteck
var box: Rect := CircleBoundingRect(umkreis);
PrintLn("Bounding-Rechteck: min(", IntToStr(box.min.x), ",", IntToStr(box.min.y),
") max(", IntToStr(box.max.x), ",", IntToStr(box.max.y), ")");
// Ueberschneidung mit einem Rechteck
PrintLn("schneidet Rechteck 120..220? ", IntToStr(CircleIntersectsRect(umkreis, RectFromXYWH(120,120,100,100)) as int64));
// Zahlenbereiche
var spanne: Range := RangeNew(10, 50);
PrintLn("-- Range [10,50]: Laenge ", IntToStr(RangeLength(spanne)), ", Mitte ", IntToStr(RangeCenter(spanne)));
PrintLn("enthaelt 30? ", IntToStr(RangeContains(spanne, 30) as int64));
PrintLn("ueberschneidet [40,80]? ", IntToStr(RangeIntersects(spanne, RangeNew(40,80)) as int64));
var s: Range := RangeIntersection(spanne, RangeNew(40, 80));
PrintLn("Schnitt = [", IntToStr(s.min), ", ", IntToStr(s.max), "]");
return 0;
}
Ausgabe:
Radius 50, Durchmesser 100, Umfang 314
(100,100) mittig = 1
(150,100) am Rand = 1
(151,100) knapp draussen = 0
Abstand von (250,100) = 100
Bounding-Rechteck: min(50,50) max(150,150)
schneidet Rechteck 120..220? 1
-- Range [10,50]: Laenge 40, Mitte 30
enthaelt 30? 1
ueberschneidet [40,80]? 1
Schnitt = [40, 50]
----
===== Was nicht stimmt =====
Geprüft mit ''lyxc 1.0.21A''.
==== CircleArea liefert 0 (#1489) ====
^ Radius ^ Fläche geliefert ^ Fläche korrekt ^ Umfang geliefert ^
| 10 | **0** | 314 | 62 |
| 50 | **0** | 7 854 | 314 |
| 100 | **0** | 31 416 | 628 |
| 1000 | **3** | 3 141 593 | 6 283 |
''CircleArea'' teilt das Radiusquadrat durch 1 000 000, geht also von einem Festkomma-Radius aus; ''CircleCircumference'' rechnet mit rohen Einheiten. Für jeden Radius unter 1000 ergibt die Division bereits 0.
Der Rückgabewert 0 sieht nach einem leeren Kreis aus, während ''CircleIsEmpty'' für denselben Kreis korrekt ''false'' meldet. Bis zum Fix die Fläche selbst rechnen:
var flaeche: int64 := 3141593 * c.radius * c.radius / 1000000;
==== CircleUnion verschiebt den Mittelpunkt nicht (#1490) ====
Union von (0,0,r=100) und (300,0,r=100)
-> Mittelpunkt (0,0), Radius 250 erwartet Mittelpunkt (150,0)
Der Radius stimmt, der Mittelpunkt bleibt bei ''a''. Ursache ist eine ganzzahlige Division, die den Skalierungsfaktor für den Verbindungsvektor auf 0 abrundet. Damit reicht der Ergebniskreis nur bis 250, der zweite Kreis aber bis 400 — er wird also **nicht** umschlossen, was die einzige Zusage einer Union-Funktion ist.
Die Sonderfälle davor arbeiten korrekt: enthält ein Kreis den anderen, kommt der größere zurück; bei gleichen Mittelpunkten greift ein eigener Zweig.
----
===== Verwandte Units =====
* [[lyx_-_programmiersprache:units:vector|std.vector]] — ''Vec2'', Grundlage der Mittelpunkte
* [[lyx_-_programmiersprache:units:rect|std.rect]] — Rechtecke, für Grobfilter und ''Range2D''-Umwandlung
* [[lyx_-_programmiersprache:units:math|std.math]] — ''IntSqrt'', ''Min64''/''Max64''