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))