555 lines
24 KiB
GDScript
555 lines
24 KiB
GDScript
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))
|
||
|
||
|