extends RefCounted class_name SymmetryCore ## Symmetrie-Modus im Map Builder (Karte t_f5935023, Spec ## /opt/data/scopedvr/KARTEN_MIRROR_vc27.md, Entscheidung Sly 20.10.): ## gespiegeltes Bauen — Änderungen an einer Kartenseite landen automatisch ## auf der anderen. DIESE Datei ist M1 (Fundament): reine Mathe + Format- ## Helfer, KEIN Spiegeln von Aktionen — das kommt mit M2 (Terrain), ## M3 (Objekte) und M4 (Bau-Teile + Spawns). ## ## Spiegel-Mathematik (Spec vc27): ## Zentrum = Kartenmitte. Editor-Maps liegen zentriert (bounds ## −size/2 … +size/2), die Mitte ist also in Weltkoordinaten (0, 0) — ## map_center() rechnet sie trotzdem generell aus den bounds aus ## ((min + max) / 2), damit die Helfer auch bei asymmetrischen bounds ## korrekt bleiben. ## | Modus | Position | rot_y | ## |-------------|-----------------------------|--------------| ## | Punkt (180°)| x' = 2cx − x, z' = 2cz − z | rot + 180° | ## | Achse X | x' = 2cx − x, z' = z | −rot | ## | Achse Z | x' = x, z' = 2cz − z | −rot | ## y bleibt in ALLEN Modi unverändert (Höhe ist ein Skalar). ## Standard = Punktspiegelung (nur sie macht beide Hälften wirklich ## deckungsgleich — Achsenspiegelung dreht den Zwilling um 180°, siehe Spec). ## ## Terrain-Zell-Abbildung (Zelle (i,j), i läuft entlang x, j entlang z — ## siehe SvmapDocument.terrain_height_at): Punkt (res−1−i, res−1−j), ## Achse X (res−1−i, j), Achse Z (i, res−1−j). ## ## Partner-Link: ein Paar trägt denselben `sym`-Schlüssel (optionales Feld ## an Objekten UND Bau-Teilen). Validator-Regel (svmap_io.gd, V16): ## String, nicht leer, ≤ 64 Zeichen, höchstens 2 Objekte pro Kennung. ## Alte Karten ohne `sym` laden unverändert (Rückwärtskompatibilität). ## ## Alles static + reine Funktionen, ohne Node-Abhängigkeit → headless testbar. enum Mode { OFF = 0, # Symmetrie aus POINT = 1, # Punktspiegelung 180° um die Kartenmitte (Standard) AXIS_X = 2, # Achsenspiegelung X (links ↔ rechts) AXIS_Z = 3, # Achsenspiegelung Z (vorn ↔ hinten) } ## Toolbar-/Statuszeilen-Texte, Index = Mode-Wert. const MODE_LABELS: Array[String] = ["Aus", "Punkt", "Achse X", "Achse Z"] ## Toleranz in Metern für „liegt auf der Achse/im Zentrum“ — solche Elemente ## werden beim gespiegelten Bauen NICHT gedoppelt (Spec vc27). const ON_AXIS_TOLERANCE_M := 0.05 # ------------------------------------------------------------ Modus/Helfer -- static func mode_label(mode: int) -> String: return MODE_LABELS[clampi(mode, 0, MODE_LABELS.size() - 1)] ## Kartenmitte aus den bounds ((min + max) / 2 in X und Z). Für zentrierte ## Editor-Maps = (0, 0), unabhängig von der Kartengröße — die Größe steckt ## in den bounds. Fehlen die bounds (defektes doc) → (0, 0). static func map_center(doc: Dictionary) -> Vector2: var bounds: Dictionary = doc.get("bounds", {}) var mn: Array = bounds.get("min", []) var mx: Array = bounds.get("max", []) if (mn as Array).size() == 3 and (mx as Array).size() == 3: return Vector2( (float(mn[0]) + float(mx[0])) * 0.5, (float(mn[2]) + float(mx[2])) * 0.5) return Vector2.ZERO # ----------------------------------------------------- Transform-Helfer ----- ## Spiegelt die XZ-Position eines Objekts/Bau-Teils; Y (Höhe) bleibt gleich. static func mirror_point(mode: int, p: Vector3, center: Vector2) -> Vector3: match mode: Mode.POINT: return Vector3(2.0 * center.x - p.x, p.y, 2.0 * center.y - p.z) Mode.AXIS_X: return Vector3(2.0 * center.x - p.x, p.y, p.z) Mode.AXIS_Z: return Vector3(p.x, p.y, 2.0 * center.y - p.z) _: return p ## Spiegelt die Y-Drehung in Grad. Normalisiert auf [0, 360): zwei Anwendungen ## ergeben wieder die Ausgangsdrehung (modulo 360 — gleiche Orientierung). ## Punkt: +180°, Achsen: −180°-Spiegel = Vorzeichenwechsel. static func mirror_rot_y(mode: int, rot_y_deg: float) -> float: match mode: Mode.POINT: return fposmod(rot_y_deg + 180.0, 360.0) Mode.AXIS_X, Mode.AXIS_Z: return fposmod(-rot_y_deg, 360.0) _: return rot_y_deg ## Spiegelt eine Blickrichtung (Grad, Blick = (-sin, 0, -cos)) exakt mit: anders als ## mirror_rot_y zeigt der Pfeil danach wirklich in die gespiegelte Richtung. static func mirror_facing(mode: int, yaw_deg: float) -> float: match mode: Mode.POINT: return fposmod(yaw_deg + 180.0, 360.0) Mode.AXIS_X: return fposmod(-yaw_deg, 360.0) Mode.AXIS_Z: return fposmod(180.0 - yaw_deg, 360.0) _: return yaw_deg ## true, wenn der Punkt unter diesem Modus auf sich selbst abgebildet wird ## (Zentrum bei Punktspiegelung, Spiegelachse bei Achsenspiegelung, ± Toleranz). ## Solche Elemente werden beim gespiegelten Bauen NICHT gedoppelt. static func maps_to_self(mode: int, p: Vector3, center: Vector2, threshold_m: float = ON_AXIS_TOLERANCE_M) -> bool: match mode: Mode.POINT: return Vector2(p.x - center.x, p.z - center.y).length() < threshold_m Mode.AXIS_X: return absf(p.x - center.x) < threshold_m Mode.AXIS_Z: return absf(p.z - center.y) < threshold_m _: return true # ------------------------------------------------ Zell-Abbildung (Terrain) -- ## Rasterzelle (i, j) → gespiegelte Zelle. res = terrain.resolution. ## i läuft entlang x, j entlang z (wie terrain_height_at). OFF → unverändert. static func mirror_cell(mode: int, i: int, j: int, res: int) -> Vector2i: if res < 2: return Vector2i(i, j) var last := res - 1 match mode: Mode.POINT: return Vector2i(last - i, last - j) Mode.AXIS_X: return Vector2i(last - i, j) Mode.AXIS_Z: return Vector2i(i, last - j) _: return Vector2i(i, j) # ------------------------------------------------------------ Partner-Link -- ## sym-Kennung eines Elements lesen (leer = kein Partner). Typsicher: nur ## nicht-leere Strings gelten; defekte Felder werden still als „kein Partner“ ## behandelt (Rückwärtskompatibilität mit alten Karten). static func sym_of(obj: Dictionary) -> String: var s = obj.get("sym", "") return s if s is String else "" ## sym-Kennung setzen (mutiert das Element und gibt es zurück zum Verketten). static func set_sym(obj: Dictionary, key: String) -> Dictionary: obj["sym"] = key return obj ## sym-Kennung entfernen (Feld komplett löschen — fehlt es, wie in alten Karten). static func clear_sym(obj: Dictionary) -> Dictionary: obj.erase("sym") return obj ## Index des Partner-Elements in einer Liste (objects ODER gebaeude): ## das Element mit derselben sym-Kennung (index selbst ausgeschlossen). ## −1 = kein Partner gefunden. static func find_partner_index(list: Array, key: String, exclude_index: int = -1) -> int: if key.is_empty(): return -1 for i in list.size(): if i == exclude_index or not (list[i] is Dictionary): continue if sym_of(list[i] as Dictionary) == key: return i return -1 ## Nächste freie sym-Kennung im Dokument ("sym_1", "sym_2", …): scannt ## Objekte (inkl. Bauteile), deterministic (kein Zufall, keine Uhr). static func next_sym_key(doc: Dictionary) -> String: var used: Dictionary = {} var block: Array = doc.get("objects", []) for item in block: if not (item is Dictionary): continue var s := sym_of(item as Dictionary) if s.begins_with("sym_"): var tail := s.substr(4) if tail.is_valid_int(): used[int(tail)] = true var n := 1 while used.has(n): n += 1 return "sym_%d" % n # ---------------------------------- Spiegel-Mathe 3 Achsen (t_e166e656) ------ ## Spiegelt eine VOLLE Euler-Orientierung (Karte t_e166e656, Platzierungs- ## freiheit). Definition (dokumentiert in docs/map_format.md §objects.rot): ## - Achsenspiegelung (AXIS_X / AXIS_Z) = Konjugation der Basis mit der ## Spiegelmatrix: B' = M·B·M — für reines Y-Drehen exakt die bisherige ## Regel mirror_rot_y (−rot). ## - Punktspiegelung (POINT) = 180°-Drehung um Y: B' = Ry(180°)·B — für ## reines Y-Drehen exakt mirror_rot_y (+180°). ## Godot-Euler-Reihenfolge YXZ (Node3D-Rotation), Rückgabe in Grad. static func mirror_rot_euler(mode: int, rot_deg: Vector3) -> Vector3: var b := Basis.from_euler(Vector3( deg_to_rad(rot_deg.x), deg_to_rad(rot_deg.y), deg_to_rad(rot_deg.z))) var gespiegelt: Basis match mode: Mode.POINT: gespiegelt = Basis.from_euler(Vector3(0.0, deg_to_rad(180.0), 0.0)) * b Mode.AXIS_X: var m := Basis.from_scale(Vector3(-1.0, 1.0, 1.0)) gespiegelt = m * b * m Mode.AXIS_Z: var m := Basis.from_scale(Vector3(1.0, 1.0, -1.0)) gespiegelt = m * b * m _: return rot_deg # Normalisierung [0, 360) je Achse — gleiche Darstellung wie rot_y_deg # bisher (tests/test_object_mirror.gd erwartet z. B. 330 statt −30). return Vector3( fposmod(gespiegelt.get_euler().x * (180.0 / PI), 360.0), fposmod(gespiegelt.get_euler().y * (180.0 / PI), 360.0), fposmod(gespiegelt.get_euler().z * (180.0 / PI), 360.0))