====== 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''