M3 (t_34908425): Objekte spiegeln — Platzieren/Verschieben/Drehen/Löschen/Duplizieren inkl. Partner-Bindung (Spec vc27). ObjectOps: symmetry_place_twin/symmetry_sync_pairs/symmetry_expand_delete (Mathe aus SymmetryCore M1, keine zweite Spiegel-Mathe; Mitte/Achse nicht gedoppelt). interaction: Zwilling in ALLE Objekt-Pfade integriert (Platzieren, Drehen, Löschen, Duplizieren, Pfeiltasten, Ziehen live+Commit, Greifen live+Esc-Rückabwicklung, Spiegeln-Button) — jede Änderung = EIN Undo-Schritt. map_view_builder: schwächere Partner-Markierung über die bestehende Auswahl-Material-Logik (keine zweite Technik). Neu: tests/test_object_mirror.gd (74 checks, alle 3 Modi + Gegenprobe AUS), tests/probe_m3_partner_render.gd (Beweis-Bild).
This commit is contained in:
parent
c15c606311
commit
238ca7bb2f
6 changed files with 1001 additions and 11 deletions
|
|
@ -83,6 +83,10 @@ var _grab_primary := -1
|
||||||
var _grab_originals: Dictionary = {} # idx (int) → [x, y, z] exakt (Rückabwicklung)
|
var _grab_originals: Dictionary = {} # idx (int) → [x, y, z] exakt (Rückabwicklung)
|
||||||
var _grab_lifts: Dictionary = {} # idx (int) → Terrain-Abstand beim Anfassen
|
var _grab_lifts: Dictionary = {} # idx (int) → Terrain-Abstand beim Anfassen
|
||||||
var _grab_before: Dictionary = {} # History-Snapshot VOR dem Greifen
|
var _grab_before: Dictionary = {} # History-Snapshot VOR dem Greifen
|
||||||
|
# Symmetrie-Zwillinge beim Greifen (M3, t_34908425): Mitglied → Partner-Index;
|
||||||
|
# Originale der Partner, die NICHT selbst greifen (für die Esc-Rückabwicklung).
|
||||||
|
var _grab_twin_map: Dictionary = {} # idx (int) → partner index (int)
|
||||||
|
var _grab_twin_originals: Dictionary = {} # partner idx (int) → [x, y, z] exakt
|
||||||
|
|
||||||
|
|
||||||
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||||||
|
|
@ -92,6 +96,13 @@ func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||||||
refresh_terrain_cache() # t_4092e3a0: Cursor trifft das Gelände ab der ersten Mausbewegung
|
refresh_terrain_cache() # t_4092e3a0: Cursor trifft das Gelände ab der ersten Mausbewegung
|
||||||
|
|
||||||
|
|
||||||
|
## Symmetrie-Modus aus dem Hauptfenster lesen (M3, t_34908425). Fehlt das Feld
|
||||||
|
## (ältere Test-Doppel, Setup-Reihenfolgen), gilt Aus — nie ein harter Fehler.
|
||||||
|
func _sym_mode() -> int:
|
||||||
|
var v: Variant = main.get("sym_mode") if main != null else null
|
||||||
|
return int(v) if v is int else SymmetryCore.Mode.OFF
|
||||||
|
|
||||||
|
|
||||||
## Kamera fragt: dreht das Mausrad gerade ein Objekt/eine Vorschau
|
## Kamera fragt: dreht das Mausrad gerade ein Objekt/eine Vorschau
|
||||||
## (dann zoomt das Rad NICHT)? Im Freelook-Flug (t_c46d31c2) blockt das Rad
|
## (dann zoomt das Rad NICHT)? Im Freelook-Flug (t_c46d31c2) blockt das Rad
|
||||||
## zusätzlich ALLE Objekt-Reaktionen — dort ändert es die Grundgeschwindigkeit.
|
## zusätzlich ALLE Objekt-Reaktionen — dort ändert es die Grundgeschwindigkeit.
|
||||||
|
|
@ -287,12 +298,20 @@ func _place_object(viewport_pos: Vector2) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Poly-Budget prüfen, BEVOR etwas passiert (hilfreiche Meldung statt Fehler).
|
# Poly-Budget prüfen, BEVOR etwas passiert (hilfreiche Meldung statt Fehler).
|
||||||
|
# Symmetrie-Modus (M3, t_34908425): der Zwilling kostet Dasselbe nochmal —
|
||||||
|
# außer das Objekt liegt auf der Mitte/Achse (wird nicht gedoppelt).
|
||||||
|
var sym_mode: int = _sym_mode()
|
||||||
|
var sym_center := SymmetryCore.map_center(main.doc)
|
||||||
|
var sym_factor := 1.0
|
||||||
|
if sym_mode != SymmetryCore.Mode.OFF \
|
||||||
|
and not SymmetryCore.maps_to_self(sym_mode, foot, sym_center):
|
||||||
|
sym_factor = 2.0
|
||||||
var lib: Dictionary = main.manifest.get("objects", {})
|
var lib: Dictionary = main.manifest.get("objects", {})
|
||||||
var entry: Dictionary = lib.get(place_oid, {})
|
var entry: Dictionary = lib.get(place_oid, {})
|
||||||
var poly := float(entry.get("poly", 0))
|
var poly := float(entry.get("poly", 0))
|
||||||
var used := SvmapDocument.poly_usage(main.doc, main.manifest)
|
var used := SvmapDocument.poly_usage(main.doc, main.manifest)
|
||||||
var budget := float(SvmapIO.MAX_TOTAL_POLY)
|
var budget := float(SvmapIO.MAX_TOTAL_POLY)
|
||||||
if used + poly > budget:
|
if used + poly * sym_factor > budget:
|
||||||
main.call("_set_status",
|
main.call("_set_status",
|
||||||
"Kein Platz mehr im Poly-Budget (%s von %s belegt) — lass ein Objekt weg oder drück Strg+Z, um etwas zurückzunehmen."
|
"Kein Platz mehr im Poly-Budget (%s von %s belegt) — lass ein Objekt weg oder drück Strg+Z, um etwas zurückzunehmen."
|
||||||
% [main._fmt_int(int(used)), main._fmt_int(int(budget))], true)
|
% [main._fmt_int(int(used)), main._fmt_int(int(budget))], true)
|
||||||
|
|
@ -308,11 +327,24 @@ func _place_object(viewport_pos: Vector2) -> void:
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"rot_y_deg": float(place_rot_deg),
|
"rot_y_deg": float(place_rot_deg),
|
||||||
})
|
})
|
||||||
|
var new_index: int = (main.doc["objects"] as Array).size() - 1
|
||||||
|
# Symmetrie (M3, t_34908425): der Zwilling wandert in denselben Undo-Schritt
|
||||||
|
# (ein Commit für Quelle + Zwilling) und bekommt dieselbe sym-Kennung.
|
||||||
|
var twin_index := -1
|
||||||
|
var on_axis := false
|
||||||
|
if sym_mode != SymmetryCore.Mode.OFF:
|
||||||
|
var symres: Dictionary = ObjectOps.symmetry_place_twin(main.doc, new_index, sym_mode)
|
||||||
|
twin_index = int(symres.get("twin", -1))
|
||||||
|
on_axis = bool(symres.get("on_axis", false))
|
||||||
main.history.commit("%s platzieren" % main.object_label(place_oid),
|
main.history.commit("%s platzieren" % main.object_label(place_oid),
|
||||||
before, main.history.snapshot(["objects"]))
|
before, main.history.snapshot(["objects"]))
|
||||||
var new_index: int = (main.doc["objects"] as Array).size() - 1
|
|
||||||
main.call("_after_structure_change")
|
main.call("_after_structure_change")
|
||||||
main.call("_set_status", "%s platziert. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad) — Entf löscht es." % main.object_label(place_oid))
|
if on_axis:
|
||||||
|
main.call("_set_status", "%s platziert — liegt auf der Symmetrie-Mitte und wird NICHT gedoppelt. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad)." % main.object_label(place_oid))
|
||||||
|
elif twin_index >= 0:
|
||||||
|
main.call("_set_status", "%s platziert — der Zwilling auf der anderen Seite ist automatisch mitgesetzt (Auswahl zeigt ihn schwächer markiert). Strg+Z nimmt BEIDE zurück." % main.object_label(place_oid))
|
||||||
|
else:
|
||||||
|
main.call("_set_status", "%s platziert. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad) — Entf löscht es." % main.object_label(place_oid))
|
||||||
# Platzier-Modus bleibt aktiv (Mehrfach-Platzieren ohne erneutes Klicken),
|
# Platzier-Modus bleibt aktiv (Mehrfach-Platzieren ohne erneutes Klicken),
|
||||||
# Auswahl auf das frische Objekt legen? Nein — Ghost bleibt Vorrang; ESC beendet.
|
# Auswahl auf das frische Objekt legen? Nein — Ghost bleibt Vorrang; ESC beendet.
|
||||||
main.call("_mark_tree_selection", new_index)
|
main.call("_mark_tree_selection", new_index)
|
||||||
|
|
@ -443,6 +475,11 @@ func rotate_selected(delta_deg: float) -> void:
|
||||||
var before: Dictionary = main.history.snapshot(["objects"])
|
var before: Dictionary = main.history.snapshot(["objects"])
|
||||||
GroupOps.rotate_members(main.doc, members, delta_deg)
|
GroupOps.rotate_members(main.doc, members, delta_deg)
|
||||||
_sync_members_to_markers(members)
|
_sync_members_to_markers(members)
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge der gedrehten Auswahl auf den exakten
|
||||||
|
# Spiegel setzen (Punkt: +180°-Basis, Achse: Vorzeichen) — IM SELBEN Commit.
|
||||||
|
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||||
|
main.doc, members, _sym_mode(), selected_index)
|
||||||
|
_sync_members_to_markers(twins)
|
||||||
var tag := "rotmulti:%s" % str(members)
|
var tag := "rotmulti:%s" % str(members)
|
||||||
main.history.commit("Auswahl gedreht (%d Objekte)" % members.size(),
|
main.history.commit("Auswahl gedreht (%d Objekte)" % members.size(),
|
||||||
before, main.history.snapshot(["objects"]), tag)
|
before, main.history.snapshot(["objects"]), tag)
|
||||||
|
|
@ -478,6 +515,9 @@ func delete_selected() -> void:
|
||||||
if doomed.is_empty():
|
if doomed.is_empty():
|
||||||
clear_selection(true)
|
clear_selection(true)
|
||||||
return
|
return
|
||||||
|
# Symmetrie (M3, t_34908425): die Partner der Auswahl löschen sich MIT —
|
||||||
|
# ein Paar lebt oder stirbt zusammen, IM SELBEN Undo-Schritt.
|
||||||
|
doomed = ObjectOps.symmetry_expand_delete(main.doc, doomed, _sym_mode())
|
||||||
var before: Dictionary = main.history.snapshot(["objects"])
|
var before: Dictionary = main.history.snapshot(["objects"])
|
||||||
var label: String
|
var label: String
|
||||||
if doomed.size() == 1:
|
if doomed.size() == 1:
|
||||||
|
|
@ -514,16 +554,31 @@ func duplicate_selected() -> void:
|
||||||
if sources.is_empty():
|
if sources.is_empty():
|
||||||
main.call("_set_status", "Nichts ausgewählt — klicke zuerst ein Objekt an, das du kopieren willst.", true)
|
main.call("_set_status", "Nichts ausgewählt — klicke zuerst ein Objekt an, das du kopieren willst.", true)
|
||||||
return
|
return
|
||||||
if objects.size() + sources.size() > SvmapIO.MAX_OBJECTS:
|
if objects.size() + sources.size() > SvmapIO.MAX_OBJECTS \
|
||||||
|
and _sym_mode() == SymmetryCore.Mode.OFF:
|
||||||
main.call("_set_status", "Limit erreicht: In eine Map passen maximal %d Objekte — das Löschen von Objekten macht wieder Platz." % SvmapIO.MAX_OBJECTS, true)
|
main.call("_set_status", "Limit erreicht: In eine Map passen maximal %d Objekte — das Löschen von Objekten macht wieder Platz." % SvmapIO.MAX_OBJECTS, true)
|
||||||
return
|
return
|
||||||
var lib: Dictionary = main.manifest.get("objects", {})
|
var lib: Dictionary = main.manifest.get("objects", {})
|
||||||
|
var sym_mode: int = _sym_mode()
|
||||||
|
var sym_center := SymmetryCore.map_center(main.doc)
|
||||||
|
var twins_planned := 0
|
||||||
var poly_sum := 0.0
|
var poly_sum := 0.0
|
||||||
for i in sources:
|
for i in sources:
|
||||||
var src_obj: Dictionary = objects[i]
|
var src_obj: Dictionary = objects[i]
|
||||||
var scl := float(src_obj.get("scale", 1.0))
|
var scl := float(src_obj.get("scale", 1.0))
|
||||||
var entry: Dictionary = lib.get(str(src_obj.get("id", "")), {})
|
var entry: Dictionary = lib.get(str(src_obj.get("id", "")), {})
|
||||||
poly_sum += float(entry.get("poly", 0)) * scl * scl
|
poly_sum += float(entry.get("poly", 0)) * scl * scl
|
||||||
|
# Symmetrie (M3, t_34908425): JEDE Kopie bekommt einen Zwilling (außer
|
||||||
|
# auf der Mitte/Achse) — Budget und Objekt-Limit VORAB mitrechnen.
|
||||||
|
if sym_mode != SymmetryCore.Mode.OFF:
|
||||||
|
var spos: Array = src_obj.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if (spos as Array).size() == 3 and not SymmetryCore.maps_to_self(
|
||||||
|
sym_mode, Vector3(float(spos[0]), float(spos[1]), float(spos[2])), sym_center):
|
||||||
|
twins_planned += 1
|
||||||
|
poly_sum += float(entry.get("poly", 0)) * scl * scl
|
||||||
|
if twins_planned > 0 and objects.size() + sources.size() + twins_planned > SvmapIO.MAX_OBJECTS:
|
||||||
|
main.call("_set_status", "Limit erreicht: In eine Map passen maximal %d Objekte — mit Symmetrie-Zwilllingen bräuchte es %d. Das Löschen von Objekten macht wieder Platz." % [SvmapIO.MAX_OBJECTS, objects.size() + sources.size() + twins_planned], true)
|
||||||
|
return
|
||||||
var used := SvmapDocument.poly_usage(main.doc, main.manifest)
|
var used := SvmapDocument.poly_usage(main.doc, main.manifest)
|
||||||
var budget := float(SvmapIO.MAX_TOTAL_POLY)
|
var budget := float(SvmapIO.MAX_TOTAL_POLY)
|
||||||
if used + poly_sum > budget:
|
if used + poly_sum > budget:
|
||||||
|
|
@ -536,6 +591,15 @@ func duplicate_selected() -> void:
|
||||||
if new_indices.is_empty():
|
if new_indices.is_empty():
|
||||||
clear_selection(true)
|
clear_selection(true)
|
||||||
return
|
return
|
||||||
|
# Symmetrie (M3, t_34908425): jede Kopie bekommt ihren Zwilling (exakter
|
||||||
|
# Spiegel der KOPIE, neue sym-Kennung) — im selben Commit wie die Kopien.
|
||||||
|
var sym_twins: Array = []
|
||||||
|
if sym_mode != SymmetryCore.Mode.OFF:
|
||||||
|
for c in new_indices:
|
||||||
|
if c is int:
|
||||||
|
var sres: Dictionary = ObjectOps.symmetry_place_twin(main.doc, c, sym_mode)
|
||||||
|
if int(sres.get("twin", -1)) >= 0:
|
||||||
|
sym_twins.append(int(sres["twin"]))
|
||||||
var label: String
|
var label: String
|
||||||
if new_indices.size() == 1:
|
if new_indices.size() == 1:
|
||||||
label = "%s duplizieren" % main.object_label(str((objects[sources[0]] as Dictionary).get("id", "")))
|
label = "%s duplizieren" % main.object_label(str((objects[sources[0]] as Dictionary).get("id", "")))
|
||||||
|
|
@ -543,9 +607,15 @@ func duplicate_selected() -> void:
|
||||||
label = "%d Objekte duplizieren" % new_indices.size()
|
label = "%d Objekte duplizieren" % new_indices.size()
|
||||||
main.history.commit(label, before, main.history.snapshot(["objects"]))
|
main.history.commit(label, before, main.history.snapshot(["objects"]))
|
||||||
main.call("_after_structure_change")
|
main.call("_after_structure_change")
|
||||||
# Kopien direkt anwählen — Sly kann sie gleich weiterziehen.
|
# Kopien direkt anwählen — Sly kann sie gleich weiterziehen. Die frischen
|
||||||
select_many(new_indices)
|
# Zwillinge wandern mit in die Auswahl (beide Seiten sichtbar markiert).
|
||||||
main.call("_set_status", "%s: die Kopie(n) liegen +2 m versetzt und sind direkt angewählt — Ziehen verschiebt, Entf löscht, Strg+Z nimmt ALLE Kopien auf einmal zurück." % label)
|
var selected_new: Array = new_indices.duplicate()
|
||||||
|
selected_new.append_array(sym_twins)
|
||||||
|
select_many(selected_new)
|
||||||
|
if sym_twins.is_empty():
|
||||||
|
main.call("_set_status", "%s: die Kopie(n) liegen +2 m versetzt und sind direkt angewählt — Ziehen verschiebt, Entf löscht, Strg+Z nimmt ALLE Kopien auf einmal zurück." % label)
|
||||||
|
else:
|
||||||
|
main.call("_set_status", "%s: %d Kopie(n) + %d Zwilling(e) sind direkt angewählt — jede Kopie bildet mit ihrem Zwilling ein neues Paar. Strg+Z nimmt ALLE auf einmal zurück." % [label, new_indices.size(), sym_twins.size()])
|
||||||
|
|
||||||
|
|
||||||
## ▲/▼ / Strg+Bild↑/↓: AUSWAHL in 0,25-m-Schritten anheben/absenken
|
## ▲/▼ / Strg+Bild↑/↓: AUSWAHL in 0,25-m-Schritten anheben/absenken
|
||||||
|
|
@ -670,6 +740,11 @@ func nudge_selection(delta: Vector3) -> void:
|
||||||
if changed == 0:
|
if changed == 0:
|
||||||
return # am Map-Rand geklemmt — Snapshot verwerfen, kein Commit
|
return # am Map-Rand geklemmt — Snapshot verwerfen, kein Commit
|
||||||
_sync_members_to_markers(members)
|
_sync_members_to_markers(members)
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge mitführen — exakter Spiegel der
|
||||||
|
# verschobenen Quelle, im selben Commit (Pfeiltasten = 1 Undo-Schritt).
|
||||||
|
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||||
|
main.doc, members, _sym_mode())
|
||||||
|
_sync_members_to_markers(twins)
|
||||||
var desc := "Objekt verschoben (Pfeiltaste)" if members.size() == 1 \
|
var desc := "Objekt verschoben (Pfeiltaste)" if members.size() == 1 \
|
||||||
else "%d Objekte verschoben (Pfeiltasten)" % members.size()
|
else "%d Objekte verschoben (Pfeiltasten)" % members.size()
|
||||||
main.history.commit(desc, before, main.history.snapshot(["objects"]))
|
main.history.commit(desc, before, main.history.snapshot(["objects"]))
|
||||||
|
|
@ -713,6 +788,25 @@ func grab_begin() -> bool:
|
||||||
_grab_originals[i] = [float(pos[0]), float(pos[1]), float(pos[2])]
|
_grab_originals[i] = [float(pos[0]), float(pos[1]), float(pos[2])]
|
||||||
_grab_lifts[i] = float(pos[1]) - SvmapDocument.terrain_height_at(
|
_grab_lifts[i] = float(pos[1]) - SvmapDocument.terrain_height_at(
|
||||||
main.doc, float(pos[0]), float(pos[2]))
|
main.doc, float(pos[0]), float(pos[2]))
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge der Greif-Menge merken — sie ziehen
|
||||||
|
# live mit (exakter Spiegel) und Esc setzt sie EXAKT zurück.
|
||||||
|
_grab_twin_map = {}
|
||||||
|
_grab_twin_originals = {}
|
||||||
|
if _sym_mode() != SymmetryCore.Mode.OFF:
|
||||||
|
for i in members:
|
||||||
|
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var sym_key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||||||
|
if sym_key.is_empty():
|
||||||
|
continue
|
||||||
|
var t := SymmetryCore.find_partner_index(objects, sym_key, i)
|
||||||
|
if t < 0:
|
||||||
|
continue
|
||||||
|
_grab_twin_map[i] = t
|
||||||
|
if not members.has(t) and not _grab_twin_originals.has(t):
|
||||||
|
var tpos: Array = (objects[t] as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if (tpos as Array).size() == 3:
|
||||||
|
_grab_twin_originals[t] = [float(tpos[0]), float(tpos[1]), float(tpos[2])]
|
||||||
_grabbing = true
|
_grabbing = true
|
||||||
_grab_before = main.history.snapshot(["objects"])
|
_grab_before = main.history.snapshot(["objects"])
|
||||||
main.call("_set_status", "GREIFEN an: die Auswahl hängt an der Maus — Linksklick setzt sie neu (mit Raster-Snap), Esc bricht ab und setzt zurück.")
|
main.call("_set_status", "GREIFEN an: die Auswahl hängt an der Maus — Linksklick setzt sie neu (mit Raster-Snap), Esc bricht ab und setzt zurück.")
|
||||||
|
|
@ -741,6 +835,9 @@ func grab_move(hit: Vector3) -> void:
|
||||||
var marker: Node3D = view_builder.object_markers[i]
|
var marker: Node3D = view_builder.object_markers[i]
|
||||||
if marker != null and is_instance_valid(marker):
|
if marker != null and is_instance_valid(marker):
|
||||||
marker.position = Vector3(nx, ny, nz)
|
marker.position = Vector3(nx, ny, nz)
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge live mitziehen — doc + Marker,
|
||||||
|
# exakter Spiegel der gegriffenen Quelle.
|
||||||
|
_sym_grab_twin_move()
|
||||||
|
|
||||||
|
|
||||||
## Linksklick im Greifen: Positionen festschreiben — EIN Undo-Schritt.
|
## Linksklick im Greifen: Positionen festschreiben — EIN Undo-Schritt.
|
||||||
|
|
@ -755,6 +852,8 @@ func grab_confirm() -> void:
|
||||||
_grab_originals = {}
|
_grab_originals = {}
|
||||||
_grab_lifts = {}
|
_grab_lifts = {}
|
||||||
_grab_before = {}
|
_grab_before = {}
|
||||||
|
_grab_twin_map = {}
|
||||||
|
_grab_twin_originals = {}
|
||||||
_mark_dirty()
|
_mark_dirty()
|
||||||
main.call("_refresh_history_buttons")
|
main.call("_refresh_history_buttons")
|
||||||
main.call("_refresh_stats")
|
main.call("_refresh_stats")
|
||||||
|
|
@ -776,13 +875,101 @@ func grab_cancel() -> void:
|
||||||
var marker: Node3D = view_builder.object_markers[i]
|
var marker: Node3D = view_builder.object_markers[i]
|
||||||
if marker != null and is_instance_valid(marker):
|
if marker != null and is_instance_valid(marker):
|
||||||
marker.position = Vector3(float(orig[0]), float(orig[1]), float(orig[2]))
|
marker.position = Vector3(float(orig[0]), float(orig[1]), float(orig[2]))
|
||||||
|
# Symmetrie (M3, t_34908425): die MITgezogenen Zwillinge exakt zurücksetzen.
|
||||||
|
for t in _grab_twin_originals:
|
||||||
|
var torig: Array = _grab_twin_originals[t]
|
||||||
|
(objects[t] as Dictionary)["pos"] = [float(torig[0]), float(torig[1]), float(torig[2])]
|
||||||
|
if t < view_builder.object_markers.size():
|
||||||
|
var tmarker: Node3D = view_builder.object_markers[t]
|
||||||
|
if tmarker != null and is_instance_valid(tmarker):
|
||||||
|
tmarker.position = Vector3(float(torig[0]), float(torig[1]), float(torig[2]))
|
||||||
_grab_members = []
|
_grab_members = []
|
||||||
_grab_originals = {}
|
_grab_originals = {}
|
||||||
_grab_lifts = {}
|
_grab_lifts = {}
|
||||||
_grab_before = {}
|
_grab_before = {}
|
||||||
|
_grab_twin_map = {}
|
||||||
|
_grab_twin_originals = {}
|
||||||
main.call("_set_status", "Greifen abgebrochen — alles ist wieder, wie es war.")
|
main.call("_set_status", "Greifen abgebrochen — alles ist wieder, wie es war.")
|
||||||
|
|
||||||
|
|
||||||
|
## Symmetrie (M3): Zwillinge beim GREIFEN live mitziehen — doc + Marker,
|
||||||
|
## exakter Spiegel der Quelle. Gehören BEIDE Seiten eines Paares zur Greif-
|
||||||
|
## Menge, ist die Quelle das Primär-Objekt (sonst der niedrigere Index).
|
||||||
|
func _sym_grab_twin_move() -> void:
|
||||||
|
var sym_mode: int = _sym_mode()
|
||||||
|
if sym_mode == SymmetryCore.Mode.OFF or _grab_twin_map.is_empty():
|
||||||
|
return
|
||||||
|
var objects: Array = main.doc.get("objects", [])
|
||||||
|
var center := SymmetryCore.map_center(main.doc)
|
||||||
|
var handled: Dictionary = {}
|
||||||
|
for i in _grab_members:
|
||||||
|
if not _grab_twin_map.has(i):
|
||||||
|
continue
|
||||||
|
var t: int = int(_grab_twin_map[i])
|
||||||
|
var pair_key := "%d|%d" % [mini(i, t), maxi(i, t)]
|
||||||
|
if handled.has(pair_key):
|
||||||
|
continue
|
||||||
|
handled[pair_key] = true
|
||||||
|
var src_i: int = i
|
||||||
|
if _grab_members.has(t):
|
||||||
|
if _grab_primary == t:
|
||||||
|
src_i = t
|
||||||
|
elif _grab_primary != i and t < i:
|
||||||
|
src_i = t
|
||||||
|
var twin_i: int = t if src_i == i else i
|
||||||
|
if src_i >= objects.size() or not (objects[src_i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var spos: Array = (objects[src_i] as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if (spos as Array).size() != 3:
|
||||||
|
continue
|
||||||
|
var mp := SymmetryCore.mirror_point(sym_mode,
|
||||||
|
Vector3(float(spos[0]), float(spos[1]), float(spos[2])), center)
|
||||||
|
(objects[twin_i] as Dictionary)["pos"] = [mp.x, mp.y, mp.z]
|
||||||
|
if twin_i < view_builder.object_markers.size():
|
||||||
|
var tm: Node3D = view_builder.object_markers[twin_i]
|
||||||
|
if tm != null and is_instance_valid(tm):
|
||||||
|
tm.position = Vector3(mp.x, mp.y, mp.z)
|
||||||
|
|
||||||
|
|
||||||
|
## Symmetrie (M3): Zwillinge beim ZIEHEN live mitführen (nur Marker — das doc
|
||||||
|
## schreibt _commit_drag über symmetry_sync_pairs). Quelle = Primär-Objekt,
|
||||||
|
## wenn beide Seiten eines Paares in der Zieh-Menge sind, sonst niedrigerer Index.
|
||||||
|
func _sym_drag_sync_live() -> void:
|
||||||
|
var sym_mode: int = _sym_mode()
|
||||||
|
if sym_mode == SymmetryCore.Mode.OFF or view_builder == null:
|
||||||
|
return
|
||||||
|
var objects: Array = main.doc.get("objects", [])
|
||||||
|
var center := SymmetryCore.map_center(main.doc)
|
||||||
|
var handled: Dictionary = {}
|
||||||
|
for i in _drag_members:
|
||||||
|
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
var t := SymmetryCore.find_partner_index(objects, key, i)
|
||||||
|
if t < 0:
|
||||||
|
continue
|
||||||
|
var pair_key := "%d|%d" % [mini(i, t), maxi(i, t)]
|
||||||
|
if handled.has(pair_key):
|
||||||
|
continue
|
||||||
|
handled[pair_key] = true
|
||||||
|
var src_i: int = i
|
||||||
|
if _drag_members.has(t):
|
||||||
|
if _drag_primary == t:
|
||||||
|
src_i = t
|
||||||
|
elif _drag_primary != i and t < i:
|
||||||
|
src_i = t
|
||||||
|
var twin_i: int = t if src_i == i else i
|
||||||
|
if src_i >= view_builder.object_markers.size() or twin_i >= view_builder.object_markers.size():
|
||||||
|
continue
|
||||||
|
var sm: Node3D = view_builder.object_markers[src_i]
|
||||||
|
var tm: Node3D = view_builder.object_markers[twin_i]
|
||||||
|
if sm == null or tm == null or not is_instance_valid(sm) or not is_instance_valid(tm):
|
||||||
|
continue
|
||||||
|
tm.position = SymmetryCore.mirror_point(sym_mode, sm.position, center)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## ⇄ Spiegeln (X: links↔rechts, Z: vor↔zurück) an der Stelle des Objekts —
|
## ⇄ Spiegeln (X: links↔rechts, Z: vor↔zurück) an der Stelle des Objekts —
|
||||||
|
|
@ -803,6 +990,11 @@ func mirror_selected(axis: String) -> void:
|
||||||
var marker: Node3D = view_builder.object_markers[selected_index] if selected_index < view_builder.object_markers.size() else null
|
var marker: Node3D = view_builder.object_markers[selected_index] if selected_index < view_builder.object_markers.size() else null
|
||||||
if marker != null and is_instance_valid(marker):
|
if marker != null and is_instance_valid(marker):
|
||||||
marker.rotation.y = deg_to_rad(new_rot)
|
marker.rotation.y = deg_to_rad(new_rot)
|
||||||
|
# Symmetrie (M3, t_34908425): der Zwilling dreht MIT (exakter Spiegel der
|
||||||
|
# neuen Blickrichtung) — im selben Commit wie die Quelle.
|
||||||
|
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||||
|
main.doc, [selected_index], _sym_mode(), selected_index)
|
||||||
|
_sync_members_to_markers(twins)
|
||||||
var desc := "Objekt gespiegelt (links↔rechts)" if axis == "x" else "Objekt gespiegelt (vor↔zurück)"
|
var desc := "Objekt gespiegelt (links↔rechts)" if axis == "x" else "Objekt gespiegelt (vor↔zurück)"
|
||||||
main.history.commit(desc, before, main.history.snapshot(["objects"]))
|
main.history.commit(desc, before, main.history.snapshot(["objects"]))
|
||||||
main.dirty = true
|
main.dirty = true
|
||||||
|
|
@ -1011,6 +1203,9 @@ func _drag_members_move(viewport_pos: Vector2) -> void:
|
||||||
np.y = SvmapDocument.terrain_height_at(main.doc, np.x, np.z) \
|
np.y = SvmapDocument.terrain_height_at(main.doc, np.x, np.z) \
|
||||||
+ float(_drag_lifts.get(i, 0.0))
|
+ float(_drag_lifts.get(i, 0.0))
|
||||||
marker.position = np
|
marker.position = np
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge LIVE mitziehen (nur Marker —
|
||||||
|
# das doc schreibt _commit_drag), exakter Spiegel der gezogenen Quelle.
|
||||||
|
_sym_drag_sync_live()
|
||||||
|
|
||||||
|
|
||||||
func _on_left_release(mb: InputEventMouseButton) -> void:
|
func _on_left_release(mb: InputEventMouseButton) -> void:
|
||||||
|
|
@ -1079,6 +1274,11 @@ func _commit_drag() -> void:
|
||||||
for i in moved:
|
for i in moved:
|
||||||
var pos: Vector3 = (view_builder.object_markers[i] as Node3D).position
|
var pos: Vector3 = (view_builder.object_markers[i] as Node3D).position
|
||||||
objects[i]["pos"] = [float(pos.x), float(pos.y), float(pos.z)]
|
objects[i]["pos"] = [float(pos.x), float(pos.y), float(pos.z)]
|
||||||
|
# Symmetrie (M3, t_34908425): Zwillinge der gezogenen Auswahl auf den exakten
|
||||||
|
# Spiegel setzen — IM SELBEN Commit (ein Zug = EIN Undo-Schritt für beide).
|
||||||
|
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||||
|
main.doc, moved, _sym_mode(), _drag_primary)
|
||||||
|
_sync_members_to_markers(twins)
|
||||||
var desc: String
|
var desc: String
|
||||||
if _drag_is_group:
|
if _drag_is_group:
|
||||||
desc = "Gruppe verschoben (%d Objekte)" % moved.size()
|
desc = "Gruppe verschoben (%d Objekte)" % moved.size()
|
||||||
|
|
|
||||||
|
|
@ -223,3 +223,126 @@ static func placement_foot(doc: Dictionary, hit: Vector3, grid_enabled: bool,
|
||||||
pos.z = clampf(pos.z, -half.y, half.y)
|
pos.z = clampf(pos.z, -half.y, half.y)
|
||||||
pos.y = SvmapDocument.terrain_height_at(doc, pos.x, pos.z)
|
pos.y = SvmapDocument.terrain_height_at(doc, pos.x, pos.z)
|
||||||
return pos
|
return pos
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------- Symmetrie — Objekte (M3) -----
|
||||||
|
# Karte t_34908425 (Spec /opt/data/scopedvr/KARTEN_MIRROR_vc27.md): im
|
||||||
|
# Symmetrie-Modus wirkt jede Objekt-Änderung auf beide Seiten. Die Transform-
|
||||||
|
# Mathe und das sym-Feld kommen aus SymmetryCore (M1, t_f5935023) — hier nur
|
||||||
|
# die Objekt-Anwendung, KEINE zweite Spiegel-Mathe. Alles static + pure
|
||||||
|
# (Dokument rein, Mutation im Dokument), headless testbar via
|
||||||
|
# tests/test_object_mirror.gd. Modus OFF = no-op (Verhalten wie bisher).
|
||||||
|
#
|
||||||
|
# Partner-Regel (invariant): der Zwilling ist IMMER der exakte Spiegel seiner
|
||||||
|
# Quelle — Position via SymmetryCore.mirror_point (y bleibt 1:1, Spec-Tabelle),
|
||||||
|
# Drehung via SymmetryCore.mirror_rot_y (Punkt: +180°, Achse: Vorzeichen).
|
||||||
|
#
|
||||||
|
# ACHTUNG Indizes: symmetry_place_twin hängt den Zwilling ans ENDE der Liste —
|
||||||
|
# bestehende Indizes bleiben gültig (nur Anhängen, nie Verschieben).
|
||||||
|
|
||||||
|
## Zwilling beim PLATZIEREN/DUPLIZIEREN anlegen: objects[index] bekommt eine
|
||||||
|
## neue sym-Kennung (SymmetryCore.next_sym_key), der Zwilling mit exakt
|
||||||
|
## gespiegeltem Transform wandert ans ENDE der objects-Liste. Ein Objekt, das
|
||||||
|
## auf der Achse/im Zentrum liegt (SymmetryCore.maps_to_self), wird NICHT
|
||||||
|
## gedoppelt. Modus OFF oder ungültiger Index: es passiert nichts.
|
||||||
|
## Rückgabe: {"twin": int, "on_axis": bool} — twin = Index des Zwillings
|
||||||
|
## (-1 = keiner angelegt), on_axis = Objekt liegt auf Mitte/Achse.
|
||||||
|
static func symmetry_place_twin(doc: Dictionary, index: int, mode: int) -> Dictionary:
|
||||||
|
var out := {"twin": -1, "on_axis": false}
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return out
|
||||||
|
var objects: Array = doc.get("objects", [])
|
||||||
|
if index < 0 or index >= objects.size() or not (objects[index] is Dictionary):
|
||||||
|
return out
|
||||||
|
var src: Dictionary = objects[index]
|
||||||
|
var pos: Array = src.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if (pos as Array).size() != 3:
|
||||||
|
return out
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
var p := Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||||
|
if SymmetryCore.maps_to_self(mode, p, center):
|
||||||
|
out["on_axis"] = true
|
||||||
|
return out
|
||||||
|
var twin: Dictionary = src.duplicate(true)
|
||||||
|
SymmetryCore.clear_sym(twin)
|
||||||
|
var mp := SymmetryCore.mirror_point(mode, p, center)
|
||||||
|
# y 1:1 übernommen (Spec vc27: Höhe ist ein Skalar) → der Zwilling ist der
|
||||||
|
# EXAKTE Spiegel, inklusive Höhe (auf symmetrischem Terrain = auf dem Boden).
|
||||||
|
twin["pos"] = [mp.x, mp.y, mp.z]
|
||||||
|
twin["rot_y_deg"] = SymmetryCore.mirror_rot_y(mode, float(src.get("rot_y_deg", 0.0)))
|
||||||
|
var key := SymmetryCore.next_sym_key(doc)
|
||||||
|
SymmetryCore.set_sym(src, key)
|
||||||
|
SymmetryCore.set_sym(twin, key)
|
||||||
|
objects.append(twin)
|
||||||
|
out["twin"] = objects.size() - 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Nach einer Positions-/Dreh-Änderung ALLE betroffenen Paare synchronisieren:
|
||||||
|
## der Zwilling wird auf den exakten Spiegel seiner Quelle gesetzt. members =
|
||||||
|
## Indizes der geänderten Objekte; gehört BEIDE Seiten eines Paares zur
|
||||||
|
## Änderung, ist die Quelle das primary_hint (das gezogene/gedrehte Primär-
|
||||||
|
## Objekt), sonst der niedrigere Index (deterministisch). Modus OFF: no-op.
|
||||||
|
## Rückgabe: Indizes der neu geschriebenen Zwillinge (für die Marker-Synchronisierung).
|
||||||
|
static func symmetry_sync_pairs(doc: Dictionary, members: Array, mode: int,
|
||||||
|
primary_hint: int = -1) -> Array:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return []
|
||||||
|
var objects: Array = doc.get("objects", [])
|
||||||
|
var center := SymmetryCore.map_center(doc)
|
||||||
|
var changed: Array = []
|
||||||
|
var handled: Dictionary = {}
|
||||||
|
for i in members:
|
||||||
|
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
var t := SymmetryCore.find_partner_index(objects, key, i)
|
||||||
|
if t < 0:
|
||||||
|
continue
|
||||||
|
var pair_key := "%d|%d" % [mini(i, t), maxi(i, t)]
|
||||||
|
if handled.has(pair_key):
|
||||||
|
continue
|
||||||
|
handled[pair_key] = true
|
||||||
|
var src_i: int = i
|
||||||
|
if members.has(t):
|
||||||
|
# Beide Seiten in der Änderung (z. B. beide markiert und gezogen):
|
||||||
|
# die Quelle entscheidet, der Zwilling wird neu gespiegelt — so
|
||||||
|
# bleibt das Paar exakt spiegelbildlich statt auseinanderzulaufen.
|
||||||
|
if primary_hint == t:
|
||||||
|
src_i = t
|
||||||
|
elif primary_hint != i and t < i:
|
||||||
|
src_i = t
|
||||||
|
var twin_i: int = t if src_i == i else i
|
||||||
|
var src: Dictionary = objects[src_i]
|
||||||
|
var spos: Array = src.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
if (spos as Array).size() != 3:
|
||||||
|
continue
|
||||||
|
var mp := SymmetryCore.mirror_point(mode,
|
||||||
|
Vector3(float(spos[0]), float(spos[1]), float(spos[2])), center)
|
||||||
|
var twin: Dictionary = objects[twin_i]
|
||||||
|
twin["pos"] = [mp.x, mp.y, mp.z]
|
||||||
|
twin["rot_y_deg"] = SymmetryCore.mirror_rot_y(mode, float(src.get("rot_y_deg", 0.0)))
|
||||||
|
changed.append(twin_i)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
## Löschen erweitern: zu den gewählten Indizes kommen die sym-Partner dazu
|
||||||
|
## (jeder Partner genau einmal, auch wenn mehrere Quellen dasselbe Paar
|
||||||
|
## treffen). Modus OFF: Liste unverändert (Verhalten wie bisher).
|
||||||
|
static func symmetry_expand_delete(doc: Dictionary, doomed: Array, mode: int) -> Array:
|
||||||
|
if mode == SymmetryCore.Mode.OFF:
|
||||||
|
return doomed
|
||||||
|
var objects: Array = doc.get("objects", [])
|
||||||
|
var out: Array = doomed.duplicate()
|
||||||
|
for i in doomed:
|
||||||
|
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
var t := SymmetryCore.find_partner_index(objects, key, i)
|
||||||
|
if t >= 0 and not out.has(t):
|
||||||
|
out.append(t)
|
||||||
|
return out
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung
|
||||||
|
|
||||||
var _base_mat: StandardMaterial3D
|
var _base_mat: StandardMaterial3D
|
||||||
var _select_mat: StandardMaterial3D
|
var _select_mat: StandardMaterial3D
|
||||||
|
var _partner_mat: StandardMaterial3D # schwächere Auswahl-Farbe des sym-Zwillings (M3)
|
||||||
|
var _sym_partners: Array = [] # Indizes der Zwillinge der aktuellen Auswahl
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -62,6 +64,16 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void:
|
||||||
_select_mat.emission_enabled = true
|
_select_mat.emission_enabled = true
|
||||||
_select_mat.emission = SELECT_COLOR * 0.35
|
_select_mat.emission = SELECT_COLOR * 0.35
|
||||||
_select_mat.roughness = 0.7
|
_select_mat.roughness = 0.7
|
||||||
|
# sym-Zwilling (M3, t_34908425): GLEICHE Auswahl-Material-Logik, nur eine
|
||||||
|
# schwächere Farbe (abgedunkeltes Orange, sehr leises Glühen) — keine zweite
|
||||||
|
# Markierungs-Technik, damit die spätere dezente Auswahl (U6) automatisch
|
||||||
|
# mitprofitiert.
|
||||||
|
_partner_mat = StandardMaterial3D.new()
|
||||||
|
_partner_mat.albedo_color = SELECT_COLOR.darkened(0.45)
|
||||||
|
_partner_mat.emission_enabled = true
|
||||||
|
_partner_mat.emission = SELECT_COLOR * 0.12
|
||||||
|
_partner_mat.roughness = 0.7
|
||||||
|
_sym_partners = []
|
||||||
|
|
||||||
if data.is_empty():
|
if data.is_empty():
|
||||||
return
|
return
|
||||||
|
|
@ -272,6 +284,9 @@ func set_selected(idx: int) -> void:
|
||||||
func set_selected_multi(indices: Array) -> void:
|
func set_selected_multi(indices: Array) -> void:
|
||||||
selected_indices = indices.duplicate()
|
selected_indices = indices.duplicate()
|
||||||
selected_index = indices.back() if not indices.is_empty() else -1
|
selected_index = indices.back() if not indices.is_empty() else -1
|
||||||
|
# sym-Zwillinge (M3, t_34908425): aus dem doc ableiten — Partner der
|
||||||
|
# Auswahl, die nicht selbst ausgewählt sind, werden schwächer markiert.
|
||||||
|
_sym_partners = _sym_partners_of(indices)
|
||||||
var objects: Array = _doc_ref.get("objects", [])
|
var objects: Array = _doc_ref.get("objects", [])
|
||||||
for marker in object_markers:
|
for marker in object_markers:
|
||||||
if marker == null or not is_instance_valid(marker):
|
if marker == null or not is_instance_valid(marker):
|
||||||
|
|
@ -280,12 +295,14 @@ func set_selected_multi(indices: Array) -> void:
|
||||||
var mi: MeshInstance3D = marker.get_child(0)
|
var mi: MeshInstance3D = marker.get_child(0)
|
||||||
var echt := bool(marker.get_meta("glb", false))
|
var echt := bool(marker.get_meta("glb", false))
|
||||||
var ist_sel := _is_selected(idx)
|
var ist_sel := _is_selected(idx)
|
||||||
mi.material_override = _select_mat if ist_sel else (null if echt else _base_mat)
|
var ist_partner := not ist_sel and _is_partner(idx)
|
||||||
|
mi.material_override = _select_mat if ist_sel \
|
||||||
|
else (_partner_mat if ist_partner else (null if echt else _base_mat))
|
||||||
# Tint-Abgleich (Karte t_b0edb807): unmarkierte Objekte zeigen ihre
|
# Tint-Abgleich (Karte t_b0edb807): unmarkierte Objekte zeigen ihre
|
||||||
# doc-Farbe (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe).
|
# doc-Farbe (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe).
|
||||||
# Markierte bleiben orange; ihr Tint liegt als doc-Feld bereit und
|
# Markierte (Auswahl ODER Zwilling) bleiben orange; ihr Tint liegt als
|
||||||
# erscheint beim Abwählen.
|
# doc-Feld bereit und erscheint beim Abwählen.
|
||||||
if not ist_sel and idx >= 0 and idx < objects.size() \
|
if not ist_sel and not ist_partner and idx >= 0 and idx < objects.size() \
|
||||||
and objects[idx] is Dictionary:
|
and objects[idx] is Dictionary:
|
||||||
var obj: Dictionary = objects[idx]
|
var obj: Dictionary = objects[idx]
|
||||||
if SvmapTint.hat(obj):
|
if SvmapTint.hat(obj):
|
||||||
|
|
@ -294,6 +311,28 @@ func set_selected_multi(indices: Array) -> void:
|
||||||
SvmapTint.zuruecknehmen(marker)
|
SvmapTint.zuruecknehmen(marker)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_partner(idx: int) -> bool:
|
||||||
|
return _sym_partners.has(idx)
|
||||||
|
|
||||||
|
|
||||||
|
## sym-Partner der Auswahl aus dem doc bestimmen (M3, t_34908425): Objekte mit
|
||||||
|
## sym-Kennung, deren Partner NICHT selbst in der Auswahl ist. Keine sym-Felder
|
||||||
|
## im doc (alte Karten, Modus Aus) → leer, Verhalten wie bisher.
|
||||||
|
func _sym_partners_of(indices: Array) -> Array:
|
||||||
|
var objects: Array = _doc_ref.get("objects", [])
|
||||||
|
var out: Array = []
|
||||||
|
for i in indices:
|
||||||
|
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||||||
|
continue
|
||||||
|
var key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
var t := SymmetryCore.find_partner_index(objects, key, i)
|
||||||
|
if t >= 0 and not indices.has(t) and not out.has(t):
|
||||||
|
out.append(t)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector-
|
## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector-
|
||||||
## Änderung ohne Rebuild). Markierte Objekte zeigen weiter Auswahl-Orange —
|
## Änderung ohne Rebuild). Markierte Objekte zeigen weiter Auswahl-Orange —
|
||||||
## die Farbe liegt im doc und erscheint beim Abwählen (set_selected_multi).
|
## die Farbe liegt im doc und erscheint beim Abwählen (set_selected_multi).
|
||||||
|
|
|
||||||
72
tests/probe_m3_partner_render.gd
Normal file
72
tests/probe_m3_partner_render.gd
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Beweis-Probe (Karte t_34908425, WEGWERF — nicht Teil der Suite):
|
||||||
|
## M3 Objekte spiegeln — Beweis-Bild mit markierten Partnern. Ein Paar wird
|
||||||
|
## über den ECHTEN Editor-Pfad erzeugt (ObjectOps.symmetry_place_twin wie in
|
||||||
|
## _place_object), die Quelle über die echte Auswahl angewählt → der Zwilling
|
||||||
|
## muss die SCHWÄCHERE Partner-Markierung tragen (gleiche Material-Technik).
|
||||||
|
## Aufruf: xvfb-run -a godot4 --path . --rendering-driver opengl3 \
|
||||||
|
## -s res://tests/probe_m3_partner_render.gd
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var scene: PackedScene = load("res://scenes/main.tscn")
|
||||||
|
var main = scene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
DisplayServer.window_set_size(Vector2i(2328, 1310))
|
||||||
|
# Symmetrie-Modus PUNKT (Standard) + Paar über den echten Objekt-Pfad.
|
||||||
|
main.set("sym_mode", SymmetryCore.Mode.POINT)
|
||||||
|
(main.doc["objects"] as Array).append({
|
||||||
|
"id": "crate_wood", "pos": [14.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 30.0,
|
||||||
|
})
|
||||||
|
var res: Dictionary = ObjectOps.symmetry_place_twin(main.doc, 0, SymmetryCore.Mode.POINT)
|
||||||
|
print("PRUEF twin_index=%d (soll 1)" % int(res.get("twin", -1)))
|
||||||
|
main.call("_after_structure_change")
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var cam = main.get("camera")
|
||||||
|
if cam != null:
|
||||||
|
cam.set("distance", 34.0)
|
||||||
|
cam.call("focus_on", Vector3(0.0, 0.0, 0.0))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
# Echte Auswahl: Quelle anwählen → Zwilling schwächer markiert.
|
||||||
|
main.interaction.call("select_object", 0)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var builder = main.get("view_builder")
|
||||||
|
var m0 = (builder.object_markers[0] as Node3D).get_child(0) as MeshInstance3D
|
||||||
|
var m1 = (builder.object_markers[1] as Node3D).get_child(0) as MeshInstance3D
|
||||||
|
var select_mat: StandardMaterial3D = builder.get("_select_mat")
|
||||||
|
var partner_mat: StandardMaterial3D = builder.get("_partner_mat")
|
||||||
|
var ok := true
|
||||||
|
if m0.material_override != select_mat:
|
||||||
|
ok = false
|
||||||
|
print("PRUEF FAIL: Quelle trägt nicht das Auswahl-Material")
|
||||||
|
else:
|
||||||
|
print("PRUEF ok: Quelle = volle Auswahl-Markierung")
|
||||||
|
if m1.material_override != partner_mat:
|
||||||
|
ok = false
|
||||||
|
print("PRUEF FAIL: Zwilling trägt nicht das Partner-Material")
|
||||||
|
else:
|
||||||
|
print("PRUEF ok: Zwilling = schwächere Partner-Markierung")
|
||||||
|
# Klartext-Beleg: die zwei Materialien sind verschieden UND der Zwilling
|
||||||
|
# dunkler (schwächer) als die Auswahl.
|
||||||
|
print("PRUEF farben: select=%s partner=%s" % [
|
||||||
|
str(select_mat.albedo_color), str(partner_mat.albedo_color)])
|
||||||
|
if not partner_mat.albedo_color.is_equal_approx(select_mat.albedo_color.darkened(0.45)):
|
||||||
|
ok = false
|
||||||
|
print("PRUEF FAIL: Partner-Farbe ist nicht die abgeschwächte Auswahl-Farbe")
|
||||||
|
var pfad := OS.get_environment("PROBE_PNG")
|
||||||
|
if pfad.is_empty():
|
||||||
|
pfad = "user://probe_m3_partner.png"
|
||||||
|
var img := root.get_viewport().get_texture().get_image()
|
||||||
|
img.save_png(pfad)
|
||||||
|
print("Bild: %s (%dx%d)" % [pfad, img.get_width(), img.get_height()])
|
||||||
|
print("PROBE_M3_PARTNER_%s" % ("OK" if ok else "FAIL"))
|
||||||
|
quit(0 if ok else 1)
|
||||||
555
tests/test_object_mirror.gd
Normal file
555
tests/test_object_mirror.gd
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Editor-Tests (headless): Karte t_34908425 — M3 Objekte spiegeln
|
||||||
|
## (Platzieren/Verschieben/Drehen/Löschen/Duplizieren inkl. Partner-Bindung).
|
||||||
|
## Ausführen: godot --headless --path . --script res://tests/test_object_mirror.gd
|
||||||
|
## Exit 0 = alles grün. Die Transform-Mathe kommt aus SymmetryCore (M1) —
|
||||||
|
## hier wird die ANWENDUNG auf Objekte geprüft (ObjectOps + interaction-Pfade).
|
||||||
|
## Die Partner-Markierung (Viewport) prüft der Material-Vergleich am echten
|
||||||
|
## map_view_builder; die Optik selbst sieht Sly im Desktop-Smoke.
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
var checks := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
# Pure Tests (kein Viewport nötig) laufen direkt; Viewport-Tests
|
||||||
|
# (Kamera-Projektion) brauchen laufende Frames — wie test_library.
|
||||||
|
_test_place_twin_modes()
|
||||||
|
_test_on_axis_not_doubled()
|
||||||
|
_test_mode_off_noop()
|
||||||
|
_test_sync_pairs_move()
|
||||||
|
_test_sync_pairs_rotate()
|
||||||
|
_test_sync_pairs_both_in_set()
|
||||||
|
_test_expand_delete()
|
||||||
|
_test_undo_single_step()
|
||||||
|
_after_frames()
|
||||||
|
|
||||||
|
|
||||||
|
func _after_frames() -> void:
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
_test_interaction_placement()
|
||||||
|
_test_interaction_rotate_delete()
|
||||||
|
_test_interaction_duplicate_nudge()
|
||||||
|
_test_interaction_grab()
|
||||||
|
_test_interaction_mode_off()
|
||||||
|
_test_partner_marking()
|
||||||
|
_test_validator()
|
||||||
|
if failures == 0:
|
||||||
|
print("ALL OK: %d checks" % checks)
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
print("FAILED: %d/%d checks" % [failures, checks])
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(cond: bool, label: String) -> void:
|
||||||
|
checks += 1
|
||||||
|
if cond:
|
||||||
|
print(" ok: %s" % label)
|
||||||
|
else:
|
||||||
|
failures += 1
|
||||||
|
print(" FAIL: %s" % label)
|
||||||
|
|
||||||
|
|
||||||
|
func _obj(id: String, pos: Array, rot := 0.0) -> Dictionary:
|
||||||
|
return {"id": id, "pos": pos, "scale": 1.0, "rot_y_deg": float(rot)}
|
||||||
|
|
||||||
|
|
||||||
|
func _flat_doc() -> Dictionary:
|
||||||
|
return SvmapDocument.create_new("M3Test", 1024.0, "mittag")
|
||||||
|
|
||||||
|
|
||||||
|
func _pair_doc(mode: int, src_pos: Array, src_rot := 30.0) -> Dictionary:
|
||||||
|
# Quelle setzen + Zwilling über DEN ECHTEN Pfad anlegen (wie Platzierung).
|
||||||
|
var doc := _flat_doc()
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", src_pos, src_rot))
|
||||||
|
var res: Dictionary = ObjectOps.symmetry_place_twin(doc, 0, mode)
|
||||||
|
if int(res.get("twin", -1)) < 0:
|
||||||
|
_check(false, "pair_doc: Zwilling wurde nicht angelegt (mode %d)" % mode)
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
func _pos_eq(a: Array, b: Array, label: String) -> void:
|
||||||
|
var ok := (a as Array).size() == 3 and (b as Array).size() == 3
|
||||||
|
if ok:
|
||||||
|
for k in 3:
|
||||||
|
if absf(float(a[k]) - float(b[k])) > 0.0001:
|
||||||
|
ok = false
|
||||||
|
_check(ok, label)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Pure ObjectOps --
|
||||||
|
|
||||||
|
func _test_place_twin_modes() -> void:
|
||||||
|
print("== Platzieren legt Zwilling an (alle 3 Modi) ==")
|
||||||
|
# Punkt: x/z negiert, rot +180°.
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
_check((doc["objects"] as Array).size() == 2, "Punkt: 2 Objekte nach dem Platzieren")
|
||||||
|
var twin: Dictionary = (doc["objects"] as Array)[1]
|
||||||
|
_pos_eq(twin.get("pos", []), [-100.0, 0.0, -40.0],
|
||||||
|
"Punkt: Zwilling-Position exakt gespiegelt (−100/−40, Höhe 1:1)")
|
||||||
|
_check(absf(float(twin.get("rot_y_deg", 0.0)) - 210.0) < 0.0001,
|
||||||
|
"Punkt: Zwilling-Drehung 30° + 180° = 210° (ist %.1f)" % float(twin.get("rot_y_deg", 0.0)))
|
||||||
|
_check(SymmetryCore.sym_of((doc["objects"] as Array)[0]) != ""
|
||||||
|
and SymmetryCore.sym_of((doc["objects"] as Array)[0]) == SymmetryCore.sym_of(twin),
|
||||||
|
"Punkt: beide tragen dieselbe sym-Kennung")
|
||||||
|
# Achse X: x negiert, z bleibt, rot-Vorzeichen.
|
||||||
|
var docx := _pair_doc(SymmetryCore.Mode.AXIS_X, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var twinx: Dictionary = (docx["objects"] as Array)[1]
|
||||||
|
_pos_eq(twinx.get("pos", []), [-100.0, 0.0, 40.0], "Achse X: x gespiegelt, z bleibt")
|
||||||
|
_check(absf(float(twinx.get("rot_y_deg", 0.0)) - 330.0) < 0.0001,
|
||||||
|
"Achse X: Drehung Vorzeichenwechsel 30° → 330° (ist %.1f)" % float(twinx.get("rot_y_deg", 0.0)))
|
||||||
|
# Achse Z: z negiert, x bleibt.
|
||||||
|
var docz := _pair_doc(SymmetryCore.Mode.AXIS_Z, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var twinz: Dictionary = (docz["objects"] as Array)[1]
|
||||||
|
_pos_eq(twinz.get("pos", []), [100.0, 0.0, -40.0], "Achse Z: z gespiegelt, x bleibt")
|
||||||
|
_check(absf(float(twinz.get("rot_y_deg", 0.0)) - 330.0) < 0.0001,
|
||||||
|
"Achse Z: Drehung Vorzeichenwechsel")
|
||||||
|
# Zweites Paar → nächste freie Kennung, keine Kollision.
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [-200.0, 0.0, 0.0], 0.0))
|
||||||
|
var res2: Dictionary = ObjectOps.symmetry_place_twin(doc, 2, SymmetryCore.Mode.POINT)
|
||||||
|
_check(int(res2.get("twin", -1)) == 3, "zweites Paar: Zwilling angehängt (Index 3)")
|
||||||
|
var k1 := SymmetryCore.sym_of((doc["objects"] as Array)[0])
|
||||||
|
var k2 := SymmetryCore.sym_of((doc["objects"] as Array)[2])
|
||||||
|
_check(k1 != k2 and k2 == "sym_2", "zweites Paar: eigene Kennung sym_2 (ist %s)" % k2)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_on_axis_not_doubled() -> void:
|
||||||
|
print("== Objekt auf der Mitte/Achse wird NICHT gedoppelt ==")
|
||||||
|
var doc := _flat_doc()
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [0.0, 0.0, 0.0]))
|
||||||
|
var rp: Dictionary = ObjectOps.symmetry_place_twin(doc, 0, SymmetryCore.Mode.POINT)
|
||||||
|
_check(bool(rp.get("on_axis", false)) and int(rp.get("twin", -1)) == -1
|
||||||
|
and (doc["objects"] as Array).size() == 1,
|
||||||
|
"Punkt: Objekt im Zentrum bleibt einzeln (on_axis gemeldet)")
|
||||||
|
_check(SymmetryCore.sym_of((doc["objects"] as Array)[0]) == "",
|
||||||
|
"Punkt: Einzelobjekt auf der Mitte bekommt KEINE sym-Kennung")
|
||||||
|
var docx := _flat_doc()
|
||||||
|
(docx["objects"] as Array).append(_obj("crate_wood", [0.0, 0.0, 40.0]))
|
||||||
|
var rx: Dictionary = ObjectOps.symmetry_place_twin(docx, 0, SymmetryCore.Mode.AXIS_X)
|
||||||
|
_check(bool(rx.get("on_axis", false)) and (docx["objects"] as Array).size() == 1,
|
||||||
|
"Achse X: Objekt auf der Achse bleibt einzeln")
|
||||||
|
var docz := _flat_doc()
|
||||||
|
(docz["objects"] as Array).append(_obj("crate_wood", [40.0, 0.0, 0.0]))
|
||||||
|
var rz: Dictionary = ObjectOps.symmetry_place_twin(docz, 0, SymmetryCore.Mode.AXIS_Z)
|
||||||
|
_check(bool(rz.get("on_axis", false)) and (docz["objects"] as Array).size() == 1,
|
||||||
|
"Achse Z: Objekt auf der Achse bleibt einzeln")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_mode_off_noop() -> void:
|
||||||
|
print("== Modus AUS = Verhalten exakt wie bisher (Gegenprobe) ==")
|
||||||
|
var doc := _flat_doc()
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [100.0, 0.0, 40.0], 30.0))
|
||||||
|
var rp: Dictionary = ObjectOps.symmetry_place_twin(doc, 0, SymmetryCore.Mode.OFF)
|
||||||
|
_check(int(rp.get("twin", -1)) == -1 and not bool(rp.get("on_axis", false))
|
||||||
|
and (doc["objects"] as Array).size() == 1, "AUS: Platzieren legt KEINEN Zwilling an")
|
||||||
|
_check(SymmetryCore.sym_of((doc["objects"] as Array)[0]) == "", "AUS: keine sym-Kennung")
|
||||||
|
var moved: int = ObjectOps.nudge_objects(doc, [0], Vector3(2.0, 0.0, 0.0))
|
||||||
|
var synced: Array = ObjectOps.symmetry_sync_pairs(doc, [0], SymmetryCore.Mode.OFF)
|
||||||
|
_check(moved == 1 and synced.is_empty(), "AUS: Verschieben synchronisiert nichts")
|
||||||
|
var del: Array = ObjectOps.symmetry_expand_delete(doc, [0], SymmetryCore.Mode.OFF)
|
||||||
|
_check(del == [0], "AUS: Löschen erfasst nur die Auswahl")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_sync_pairs_move() -> void:
|
||||||
|
print("== Verschieben: Zwilling = exakter Spiegel der Quelle ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
(doc["objects"] as Array)[0]["pos"] = [120.0, 0.0, 60.0]
|
||||||
|
var synced: Array = ObjectOps.symmetry_sync_pairs(doc, [0], SymmetryCore.Mode.POINT)
|
||||||
|
_check(synced == [1], "Verschieben: Zwilling-Index 1 zurückgegeben")
|
||||||
|
_pos_eq(((doc["objects"] as Array)[1] as Dictionary).get("pos", []),
|
||||||
|
[-120.0, 0.0, -60.0], "Verschieben: Zwilling exakt mitgespiegelt")
|
||||||
|
_check(absf(float(((doc["objects"] as Array)[1] as Dictionary).get("rot_y_deg", 0.0)) - 210.0) < 0.0001,
|
||||||
|
"Verschieben: Drehung unverändert gespiegelt (210°)")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_sync_pairs_rotate() -> void:
|
||||||
|
print("== Drehen: Punkt +180°-Basis, Achse Vorzeichen ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
(doc["objects"] as Array)[0]["rot_y_deg"] = 75.0
|
||||||
|
ObjectOps.symmetry_sync_pairs(doc, [0], SymmetryCore.Mode.POINT)
|
||||||
|
_check(absf(float(((doc["objects"] as Array)[1] as Dictionary).get("rot_y_deg", 0.0)) - 255.0) < 0.0001,
|
||||||
|
"Punkt: Quelle 30→75 (+45), Zwilling 210→255 (+45, Basis +180°)")
|
||||||
|
var docx := _pair_doc(SymmetryCore.Mode.AXIS_X, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
(docx["objects"] as Array)[0]["rot_y_deg"] = 75.0
|
||||||
|
ObjectOps.symmetry_sync_pairs(docx, [0], SymmetryCore.Mode.AXIS_X)
|
||||||
|
_check(absf(float(((docx["objects"] as Array)[1] as Dictionary).get("rot_y_deg", 0.0)) - 285.0) < 0.0001,
|
||||||
|
"Achse X: Quelle 30→75, Zwilling 330→285 (Vorzeichen)")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_sync_pairs_both_in_set() -> void:
|
||||||
|
print("== Beide Seiten gemeinsam geändert: Quelle = Primär-Objekt ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
(doc["objects"] as Array)[1]["pos"] = [-90.0, 0.0, -50.0] # Zwilling wurde mitgezogen
|
||||||
|
var synced: Array = ObjectOps.symmetry_sync_pairs(
|
||||||
|
doc, [0, 1], SymmetryCore.Mode.POINT, 1)
|
||||||
|
_check(synced == [0], "beide in der Änderung: Index 0 wird als Zwilling neu gesetzt")
|
||||||
|
_pos_eq(((doc["objects"] as Array)[0] as Dictionary).get("pos", []),
|
||||||
|
[90.0, 0.0, 50.0], "Primär (Index 1) bleibt, Partner = exakter Spiegel")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_expand_delete() -> void:
|
||||||
|
print("== Löschen: Partner stirbt mit ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var doomed: Array = ObjectOps.symmetry_expand_delete(doc, [0], SymmetryCore.Mode.POINT)
|
||||||
|
_check(doomed == [0, 1], "Löschen: Auswahl 0 erweitert sich um Partner 1")
|
||||||
|
var both: Array = ObjectOps.symmetry_expand_delete(doc, [1, 0], SymmetryCore.Mode.POINT)
|
||||||
|
_check(both == [1, 0], "beide schon in der Auswahl: keine Doppel-Einträge")
|
||||||
|
|
||||||
|
|
||||||
|
func _test_undo_single_step() -> void:
|
||||||
|
print("== EINE Änderung = EIN Undo-Schritt ==")
|
||||||
|
var doc := _flat_doc()
|
||||||
|
var hist := ActionHistory.new()
|
||||||
|
hist.begin(doc)
|
||||||
|
# Platzieren mit Zwilling: ein Commit.
|
||||||
|
var before: Dictionary = hist.snapshot(["objects"])
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [100.0, 0.0, 40.0], 30.0))
|
||||||
|
ObjectOps.symmetry_place_twin(doc, 0, SymmetryCore.Mode.POINT)
|
||||||
|
hist.commit("Test: platzieren", before, hist.snapshot(["objects"]))
|
||||||
|
_check((doc["objects"] as Array).size() == 2 and hist.entries.size() == 1,
|
||||||
|
"Platzieren mit Zwilling = EIN Historie-Eintrag (2 Objekte)")
|
||||||
|
# Verschieben des Paares: ein weiterer Commit.
|
||||||
|
var b2: Dictionary = hist.snapshot(["objects"])
|
||||||
|
(doc["objects"] as Array)[0]["pos"] = [150.0, 0.0, 40.0]
|
||||||
|
ObjectOps.symmetry_sync_pairs(doc, [0], SymmetryCore.Mode.POINT)
|
||||||
|
hist.commit("Test: verschieben", b2, hist.snapshot(["objects"]))
|
||||||
|
_check(hist.entries.size() == 2, "Verschieben mit Zwilling = EIN weiterer Eintrag")
|
||||||
|
# Ein Undo → BEIDE springt auf den Stand vor dem Verschieben zurück.
|
||||||
|
hist.undo()
|
||||||
|
_pos_eq(((doc["objects"] as Array)[0] as Dictionary).get("pos", []),
|
||||||
|
[100.0, 0.0, 40.0], "Ein Undo: Quelle auf dem Ausgangspunkt")
|
||||||
|
_pos_eq(((doc["objects"] as Array)[1] as Dictionary).get("pos", []),
|
||||||
|
[-100.0, 0.0, -40.0], "Ein Undo: Zwilling auf dem gespiegelten Ausgangspunkt")
|
||||||
|
_check((doc["objects"] as Array).size() == 2, "Ein Undo: Paar existiert weiter")
|
||||||
|
# Zweites Undo → komplettes Platzieren (Quelle + Zwilling) zurück.
|
||||||
|
hist.undo()
|
||||||
|
_check((doc["objects"] as Array).is_empty(),
|
||||||
|
"Zweites Undo: Platzieren nimmt Quelle UND Zwilling komplett zurück")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- Interaction-Pfade ----
|
||||||
|
|
||||||
|
## Minimales main-Doppel: doc + echte ActionHistory + sym_mode; alle UI-Rufe
|
||||||
|
## (Statuszeile etc.) landen als No-Op im Doppel und werden mitgeschrieben.
|
||||||
|
class ProbeMain:
|
||||||
|
var doc: Dictionary = {}
|
||||||
|
var manifest: Dictionary = {}
|
||||||
|
var grid_snap := false
|
||||||
|
var sym_mode: int = SymmetryCore.Mode.OFF
|
||||||
|
var dirty := false
|
||||||
|
var history: ActionHistory = ActionHistory.new()
|
||||||
|
var last_status := ""
|
||||||
|
|
||||||
|
func _set_status(msg: String, _is_err := false) -> void:
|
||||||
|
last_status = msg
|
||||||
|
|
||||||
|
func _base_tool_status() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _highlight_library(_oid: String) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _after_structure_change() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _mark_tree_selection(_idx: int) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _mark_tree_selection_multi(_indices: Array) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _selection_status(_idx: int) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _sync_selection_panel() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _refresh_history_buttons() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _refresh_stats() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _refresh_file_label() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func object_label(oid: String) -> String:
|
||||||
|
return oid
|
||||||
|
|
||||||
|
func _fmt_int(n: int) -> String:
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
|
## Builder-Doppel: interaction liest/schreibt nur object_markers und ruft
|
||||||
|
## set_selected_multi — Marker sind echte Node3D an der Objekt-Position.
|
||||||
|
class ProbeBuilder:
|
||||||
|
var object_markers: Array = []
|
||||||
|
|
||||||
|
func set_selected_multi(_indices: Array) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func sync_markers(doc: Dictionary) -> void:
|
||||||
|
object_markers.clear()
|
||||||
|
for obj in (doc.get("objects", []) as Array):
|
||||||
|
var m := Node3D.new()
|
||||||
|
var pos: Array = (obj as Dictionary).get("pos", [0.0, 0.0, 0.0])
|
||||||
|
m.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||||||
|
m.rotation.y = deg_to_rad(float((obj as Dictionary).get("rot_y_deg", 0.0)))
|
||||||
|
object_markers.append(m)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_interaction(doc: Dictionary, sym_mode: int, vp: SubViewport,
|
||||||
|
cam: Camera3D) -> Node3D:
|
||||||
|
var fake := ProbeMain.new()
|
||||||
|
fake.doc = doc
|
||||||
|
fake.manifest = {"objects": {"crate_wood": {"poly": 0, "collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
|
||||||
|
fake.sym_mode = sym_mode
|
||||||
|
fake.history.begin(doc)
|
||||||
|
var builder := ProbeBuilder.new()
|
||||||
|
builder.sync_markers(doc)
|
||||||
|
var interaction := Node3D.new()
|
||||||
|
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||||||
|
vp.add_child(interaction)
|
||||||
|
interaction.setup(fake, cam, builder)
|
||||||
|
return interaction
|
||||||
|
|
||||||
|
|
||||||
|
func _main_of(interaction: Node3D) -> ProbeMain:
|
||||||
|
return interaction.get("main") as ProbeMain
|
||||||
|
|
||||||
|
|
||||||
|
func _builder_of(interaction: Node3D) -> ProbeBuilder:
|
||||||
|
return interaction.get("view_builder") as ProbeBuilder
|
||||||
|
|
||||||
|
|
||||||
|
func _test_interaction_placement() -> void:
|
||||||
|
print("== Platzieren über interaction._place_object ==")
|
||||||
|
var vp := SubViewport.new()
|
||||||
|
vp.size = Vector2i(1280, 720)
|
||||||
|
root.add_child(vp)
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
vp.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
cam.look_at_from_position(Vector3(-95.0, 115.0, 135.0), Vector3.ZERO, Vector3.UP)
|
||||||
|
# Modus Punkt: Klick bei (100, 0, 40) → Quelle + Zwilling.
|
||||||
|
var doc := _flat_doc()
|
||||||
|
var inter := _make_interaction(doc, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter.set("place_oid", "crate_wood")
|
||||||
|
inter.set("place_rot_deg", 30.0)
|
||||||
|
inter.set("placing", true)
|
||||||
|
var h := SvmapDocument.terrain_height_at(doc, 100.0, 40.0)
|
||||||
|
inter.call("_place_object", cam.unproject_position(Vector3(100.0, h, 40.0)))
|
||||||
|
var objs: Array = doc.get("objects", [])
|
||||||
|
_check(objs.size() == 2, "Platzieren (Punkt): 2 Objekte im doc")
|
||||||
|
if objs.size() == 2:
|
||||||
|
_pos_eq((objs[1] as Dictionary).get("pos", []), [-100.0, h, -40.0],
|
||||||
|
"Platzieren (Punkt): Zwilling exakt gespiegelt")
|
||||||
|
var k := SymmetryCore.sym_of(objs[0])
|
||||||
|
_check(k != "" and k == SymmetryCore.sym_of(objs[1]), "Platzieren: Paar-Kennung gesetzt")
|
||||||
|
var fake := _main_of(inter)
|
||||||
|
_check(fake.history.entries.size() == 1, "Platzieren mit Zwilling = EIN Undo-Schritt")
|
||||||
|
_check(fake.last_status.contains("Zwilling"), "Statuszeile meldet den Zwilling")
|
||||||
|
fake.history.undo()
|
||||||
|
_check((doc["objects"] as Array).is_empty(),
|
||||||
|
"Ein Strg+Z: Quelle UND Zwilling zurück (Map vorher leer)")
|
||||||
|
# Auf der Mitte: NICHT gedoppelt, Klartext-Meldung.
|
||||||
|
var doc2 := _flat_doc()
|
||||||
|
var inter2 := _make_interaction(doc2, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter2.set("place_oid", "crate_wood")
|
||||||
|
inter2.set("placing", true)
|
||||||
|
var h0 := SvmapDocument.terrain_height_at(doc2, 0.0, 0.0)
|
||||||
|
inter2.call("_place_object", cam.unproject_position(Vector3(0.0, h0, 0.0)))
|
||||||
|
_check((doc2["objects"] as Array).size() == 1, "Mitte: Objekt wird nicht gedoppelt")
|
||||||
|
_check((_main_of(inter2) as ProbeMain).last_status.to_lower().contains("nicht gedoppelt"),
|
||||||
|
"Statuszeile meldet „nicht gedoppelt“")
|
||||||
|
vp.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_interaction_rotate_delete() -> void:
|
||||||
|
print("== Drehen + Löschen über interaction ==")
|
||||||
|
var vp := SubViewport.new()
|
||||||
|
vp.size = Vector2i(64, 64)
|
||||||
|
root.add_child(vp)
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
vp.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
# Drehen: Quelle +15° → Zwilling +15° (Punkt-Basis +180° bleibt).
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter := _make_interaction(doc, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter.call("select_object", 0)
|
||||||
|
inter.call("rotate_selected", 15.0)
|
||||||
|
var objs: Array = doc.get("objects", [])
|
||||||
|
_check(absf(float((objs[0] as Dictionary).get("rot_y_deg", 0.0)) - 45.0) < 0.0001,
|
||||||
|
"Drehen: Quelle 30→45")
|
||||||
|
_check(absf(float((objs[1] as Dictionary).get("rot_y_deg", 0.0)) - 225.0) < 0.0001,
|
||||||
|
"Drehen: Zwilling 210→225 (dreht MIT)")
|
||||||
|
_check(absf((_builder_of(inter).object_markers[1] as Node3D).rotation.y
|
||||||
|
- deg_to_rad(225.0)) < 0.001, "Drehen: Zwilling-Marker sichtbar mitgedreht")
|
||||||
|
var fake := _main_of(inter)
|
||||||
|
_check(fake.history.entries.size() == 1, "Drehen mit Zwilling = EIN Undo-Schritt")
|
||||||
|
fake.history.undo()
|
||||||
|
_check(absf(float(((doc["objects"] as Array)[1] as Dictionary).get("rot_y_deg", 0.0)) - 210.0) < 0.0001,
|
||||||
|
"Ein Undo: beide Drehungen zurück")
|
||||||
|
# Löschen: Quelle löschen nimmt den Zwilling mit; EIN Undo holt beide.
|
||||||
|
var doc2 := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter2 := _make_interaction(doc2, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter2.call("select_object", 0)
|
||||||
|
inter2.call("delete_selected")
|
||||||
|
_check((doc2["objects"] as Array).is_empty(), "Löschen: Quelle UND Zwilling weg")
|
||||||
|
var fake2 := _main_of(inter2)
|
||||||
|
_check(fake2.history.entries.size() == 1, "Löschen mit Zwilling = EIN Undo-Schritt")
|
||||||
|
fake2.history.undo()
|
||||||
|
_check((doc2["objects"] as Array).size() == 2, "Ein Undo: beide Objekte zurück")
|
||||||
|
vp.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_interaction_duplicate_nudge() -> void:
|
||||||
|
print("== Duplizieren + Pfeiltasten über interaction ==")
|
||||||
|
var vp := SubViewport.new()
|
||||||
|
vp.size = Vector2i(64, 64)
|
||||||
|
root.add_child(vp)
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
vp.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
# Duplizieren: Kopie + Zwilling der Kopie = neues Paar, eigene Kennung.
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter := _make_interaction(doc, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter.call("select_object", 0)
|
||||||
|
inter.call("duplicate_selected")
|
||||||
|
var objs: Array = doc.get("objects", [])
|
||||||
|
_check(objs.size() == 4, "Duplizieren: 2 Paare (Kopie + Zwilling der Kopie)")
|
||||||
|
if objs.size() == 4:
|
||||||
|
var k_old := SymmetryCore.sym_of(objs[0])
|
||||||
|
var k_new := SymmetryCore.sym_of(objs[2])
|
||||||
|
_check(k_old != k_new and k_new != "", "neues Paar mit EIGENER Kennung")
|
||||||
|
_check(SymmetryCore.sym_of(objs[3]) == k_new, "Kopie + ihr Zwilling tragen dieselbe Kennung")
|
||||||
|
_pos_eq((objs[2] as Dictionary).get("pos", []), [102.0, 0.0, 40.0],
|
||||||
|
"Kopie +2 m in X (Rasterversatz)")
|
||||||
|
_pos_eq((objs[3] as Dictionary).get("pos", []), [-102.0, 0.0, -40.0],
|
||||||
|
"Zwilling der Kopie exakt gespiegelt")
|
||||||
|
var fake := _main_of(inter)
|
||||||
|
_check(fake.history.entries.size() == 1, "Duplizieren mit Zwillingen = EIN Undo-Schritt")
|
||||||
|
fake.history.undo()
|
||||||
|
_check((doc["objects"] as Array).size() == 2, "Ein Undo: alle 4 Kopien zurück")
|
||||||
|
# Pfeiltasten: Quelle +2 m in X → Zwilling −2 m (Punkt spiegelt das Delta).
|
||||||
|
var doc2 := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter2 := _make_interaction(doc2, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter2.call("select_object", 0)
|
||||||
|
inter2.call("nudge_selection", Vector3(2.0, 0.0, 0.0))
|
||||||
|
_pos_eq(((doc2["objects"] as Array)[0] as Dictionary).get("pos", []),
|
||||||
|
[102.0, 0.0, 40.0], "Pfeiltaste: Quelle +2 m")
|
||||||
|
_pos_eq(((doc2["objects"] as Array)[1] as Dictionary).get("pos", []),
|
||||||
|
[-102.0, 0.0, -40.0], "Pfeiltaste: Zwilling −2 m (mitgeführt)")
|
||||||
|
_check((_main_of(inter2) as ProbeMain).history.entries.size() == 1,
|
||||||
|
"Pfeiltaste mit Zwilling = EIN Undo-Schritt")
|
||||||
|
vp.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_interaction_grab() -> void:
|
||||||
|
print("== Greifen (G) über interaction ==")
|
||||||
|
var vp := SubViewport.new()
|
||||||
|
vp.size = Vector2i(64, 64)
|
||||||
|
root.add_child(vp)
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
vp.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter := _make_interaction(doc, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter.call("select_object", 0)
|
||||||
|
inter.call("grab_begin")
|
||||||
|
inter.call("grab_move", Vector3(150.0, 0.0, 20.0))
|
||||||
|
inter.call("grab_confirm")
|
||||||
|
_pos_eq(((doc["objects"] as Array)[0] as Dictionary).get("pos", []),
|
||||||
|
[150.0, 0.0, 20.0], "Greifen: Quelle am Ziel")
|
||||||
|
_pos_eq(((doc["objects"] as Array)[1] as Dictionary).get("pos", []),
|
||||||
|
[-150.0, 0.0, -20.0], "Greifen: Zwilling exakt gespiegelt am Ziel")
|
||||||
|
_check((_main_of(inter) as ProbeMain).history.entries.size() == 1,
|
||||||
|
"Greifen mit Zwilling = EIN Undo-Schritt")
|
||||||
|
# Esc bricht ab: Quelle UND Zwilling exakt auf den Ausgangspunkt zurück.
|
||||||
|
var doc2 := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var inter2 := _make_interaction(doc2, SymmetryCore.Mode.POINT, vp, cam)
|
||||||
|
inter2.call("select_object", 0)
|
||||||
|
inter2.call("grab_begin")
|
||||||
|
inter2.call("grab_move", Vector3(300.0, 0.0, 300.0))
|
||||||
|
inter2.call("grab_cancel")
|
||||||
|
_pos_eq(((doc2["objects"] as Array)[0] as Dictionary).get("pos", []),
|
||||||
|
[100.0, 0.0, 40.0], "Esc: Quelle exakt zurück")
|
||||||
|
_pos_eq(((doc2["objects"] as Array)[1] as Dictionary).get("pos", []),
|
||||||
|
[-100.0, 0.0, -40.0], "Esc: Zwilling exakt zurück")
|
||||||
|
_check((_main_of(inter2) as ProbeMain).history.entries.size() == 0,
|
||||||
|
"Esc: kein Historie-Eintrag")
|
||||||
|
vp.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_interaction_mode_off() -> void:
|
||||||
|
print("== Gegenprobe interaction bei Modus AUS ==")
|
||||||
|
var vp := SubViewport.new()
|
||||||
|
vp.size = Vector2i(64, 64)
|
||||||
|
root.add_child(vp)
|
||||||
|
var cam := Camera3D.new()
|
||||||
|
vp.add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
var doc := _flat_doc()
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [100.0, 0.0, 40.0], 30.0))
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [-100.0, 0.0, -40.0], 210.0))
|
||||||
|
var inter := _make_interaction(doc, SymmetryCore.Mode.OFF, vp, cam)
|
||||||
|
# Drehen berührt NUR die Auswahl (kein Zwilling-Mechanismus).
|
||||||
|
inter.call("select_object", 0)
|
||||||
|
inter.call("rotate_selected", 15.0)
|
||||||
|
_check(absf(float(((doc["objects"] as Array)[1] as Dictionary).get("rot_y_deg", 0.0)) - 210.0) < 0.0001,
|
||||||
|
"AUS: Drehen ändert das zweite Objekt NICHT")
|
||||||
|
# Löschen nimmt NUR die Auswahl mit.
|
||||||
|
inter.call("delete_selected")
|
||||||
|
_check((doc["objects"] as Array).size() == 1, "AUS: Löschen entfernt nur das gewählte Objekt")
|
||||||
|
# Duplizieren erzeugt NUR die Kopie (+2 m).
|
||||||
|
inter.call("select_object", 0)
|
||||||
|
inter.call("duplicate_selected")
|
||||||
|
_check((doc["objects"] as Array).size() == 2, "AUS: Duplizieren = eine Kopie, kein Zwilling")
|
||||||
|
vp.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_partner_marking() -> void:
|
||||||
|
print("== Partner-Markierung (echter Viewport-Bauer) ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
var builder := Node3D.new()
|
||||||
|
builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
||||||
|
root.add_child(builder)
|
||||||
|
var manifest := {"objects": {"crate_wood": {"collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
|
||||||
|
builder.call("rebuild", doc, manifest)
|
||||||
|
# Quelle (0) anwählen → Zwilling (1) schwächer markiert.
|
||||||
|
builder.call("set_selected_multi", [0])
|
||||||
|
var select_mat: StandardMaterial3D = builder.get("_select_mat")
|
||||||
|
var partner_mat: StandardMaterial3D = builder.get("_partner_mat")
|
||||||
|
var m0: MeshInstance3D = (builder.get("object_markers") as Array)[0].get_child(0)
|
||||||
|
var m1: MeshInstance3D = (builder.get("object_markers") as Array)[1].get_child(0)
|
||||||
|
_check(m0.material_override == select_mat, "Auswahl: Quelle volle Orange-Markierung")
|
||||||
|
_check(m1.material_override == partner_mat, "Zwilling: schwächere Partner-Markierung")
|
||||||
|
_check(partner_mat != select_mat and partner_mat != builder.get("_base_mat"),
|
||||||
|
"Partner-Material ist eigenes, abgeschwächtes Material (gleiche Technik)")
|
||||||
|
# Beide angewählt → beide volle Markierung, kein Partner-Zustand.
|
||||||
|
builder.call("set_selected_multi", [0, 1])
|
||||||
|
_check(m0.material_override == select_mat and m1.material_override == select_mat,
|
||||||
|
"beide angewählt: beide volle Auswahl-Markierung")
|
||||||
|
# Abwählen: Basiszustand zurück (GLB fehlt → Platzhalter mit Basis-Farbe).
|
||||||
|
builder.call("set_selected_multi", [])
|
||||||
|
_check(m0.material_override == builder.get("_base_mat")
|
||||||
|
and m1.material_override == builder.get("_base_mat"),
|
||||||
|
"ohne Auswahl: Basis-Farben (wie bisher)")
|
||||||
|
builder.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_validator() -> void:
|
||||||
|
print("== Validator V16 bleibt mit den Paaren zufrieden ==")
|
||||||
|
var doc := _pair_doc(SymmetryCore.Mode.POINT, [100.0, 0.0, 40.0], 30.0)
|
||||||
|
(doc["objects"] as Array).append(_obj("crate_wood", [-200.0, 0.0, 0.0], 0.0))
|
||||||
|
ObjectOps.symmetry_place_twin(doc, 2, SymmetryCore.Mode.POINT)
|
||||||
|
var errors: PackedStringArray = SvmapIO.validate_map(doc, {})
|
||||||
|
var sym_errors: Array = []
|
||||||
|
for e in errors:
|
||||||
|
if str(e).contains("V16") or str(e).contains("sym"):
|
||||||
|
sym_errors.append(e)
|
||||||
|
_check(sym_errors.is_empty(), "2 Paare im doc: keine V16/sym-Fehler (%s)" % str(sym_errors))
|
||||||
|
|
||||||
|
|
||||||
1
tests/test_object_mirror.gd.uid
Normal file
1
tests/test_object_mirror.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://hl3ag6m342ea
|
||||||
Loading…
Reference in a new issue