3D-Werkzeugleiste, Dreh-/Kipp-Gizmo um alle Achsen mit Undo, Test
This commit is contained in:
parent
59c932dc35
commit
93cdcb0cd4
5 changed files with 381 additions and 8 deletions
|
|
@ -522,7 +522,7 @@ func ungroup_selection() -> void:
|
|||
## Auswahl drehen (R / Mausrad / Button): bei mehreren Objekten oder einer
|
||||
## Gruppe rotiert ALLES gemeinsam um den Gruppen-Mittelpunkt. Mausrad-Ticks
|
||||
## derselben Auswahl laufen zu EINEM Undo-Schritt zusammen (merge_tag).
|
||||
func rotate_selected(delta_deg: float) -> void:
|
||||
func rotate_selected(delta_deg: float, merge_suffix: String = "") -> void:
|
||||
if selected_index < 0 or selected_indices.is_empty():
|
||||
return
|
||||
var objects: Array = main.doc.get("objects", [])
|
||||
|
|
@ -545,7 +545,7 @@ func rotate_selected(delta_deg: float) -> void:
|
|||
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%s" % [str(members), merge_suffix]
|
||||
main.history.commit("Auswahl gedreht (%d Objekte)" % members.size(),
|
||||
before, main.history.snapshot(["objects"]), tag)
|
||||
_mark_dirty()
|
||||
|
|
@ -560,7 +560,7 @@ func rotate_selected(delta_deg: float) -> void:
|
|||
## einer Gruppe kippt die GANZE Gruppe (wie beim Drehen), Mehrfachauswahl
|
||||
## kippt jedes Objekt um denselben Betrag, Symmetrie-Zwillinge laufen im
|
||||
## SELBEN Commit mit. rot/rot_y_deg bleiben über SvmapIO.rot_setzen synchron.
|
||||
func tilt_selected(axis_x: bool, delta_deg: float) -> void:
|
||||
func tilt_selected(axis_x: bool, delta_deg: float, merge_suffix: String = "") -> void:
|
||||
if selected_index < 0 or selected_indices.is_empty():
|
||||
return
|
||||
var objects: Array = main.doc.get("objects", [])
|
||||
|
|
@ -588,8 +588,8 @@ func tilt_selected(axis_x: bool, delta_deg: float) -> void:
|
|||
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||
main.doc, members, _sym_mode(), selected_index)
|
||||
_sync_members_to_markers(twins)
|
||||
var tag := "tilt:%d" % selected_index if members.size() == 1 \
|
||||
else "tiltmulti:%s" % str(members)
|
||||
var tag := ("tilt:%d" % selected_index if members.size() == 1 \
|
||||
else "tiltmulti:%s" % str(members)) + merge_suffix
|
||||
main.history.commit("Auswahl gekippt (%s, %d Objekte)" % [axis, members.size()],
|
||||
before, main.history.snapshot(["objects"]), tag)
|
||||
_mark_dirty()
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeug
|
|||
|
||||
var history := ActionHistory.new()
|
||||
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
|
||||
var viewport_toolbar # src/ui/viewport_toolbar.gd (Buttons oben links in der 3D-Ansicht)
|
||||
var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction)
|
||||
var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds)
|
||||
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
|
||||
|
|
@ -629,6 +630,11 @@ func _build_center_viewport() -> SubViewportContainer:
|
|||
world.add_child(test_start_tool)
|
||||
test_start_tool.setup(self, camera, view_builder, interaction)
|
||||
|
||||
# Werkzeugleiste oben links (Verschieben / Drehen & Kippen / Aktionen)
|
||||
viewport_toolbar = preload("res://src/ui/viewport_toolbar.gd").new()
|
||||
container.add_child(viewport_toolbar)
|
||||
viewport_toolbar.setup(self)
|
||||
|
||||
# Bedien-Hinweis im Viewport (Overlay)
|
||||
help_label = Label.new()
|
||||
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
||||
|
|
|
|||
78
src/ui/viewport_toolbar.gd
Normal file
78
src/ui/viewport_toolbar.gd
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
extends PanelContainer
|
||||
## Werkzeugleiste oben links in der 3D-Ansicht (25.10.): erscheint, sobald ein
|
||||
## Objekt ausgewählt ist. Umschalter Verschieben / Drehen & Kippen (steuert den
|
||||
## Gizmo) plus schnelle Aktionen (Auf den Boden, Duplizieren, Löschen).
|
||||
## Objekte werden bewusst NIE skaliert — es gibt kein Skalieren-Werkzeug.
|
||||
|
||||
var main
|
||||
var move_btn: Button
|
||||
var rotate_btn: Button
|
||||
var ground_btn: Button
|
||||
var dup_btn: Button
|
||||
var delete_btn: Button
|
||||
|
||||
|
||||
func setup(p_main) -> void:
|
||||
main = p_main
|
||||
set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
offset_left = UIScale.px(12)
|
||||
offset_top = UIScale.px(12)
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
visible = false
|
||||
var box := HBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", UIScale.px(6))
|
||||
add_child(box)
|
||||
var group := ButtonGroup.new()
|
||||
move_btn = _btn("↔ Verschieben", "Pfeile am Objekt ziehen: X rot, Y grün, Z blau", true, group)
|
||||
move_btn.pressed.connect(func() -> void: _set_mode("move"))
|
||||
box.add_child(move_btn)
|
||||
rotate_btn = _btn("⟳ Drehen / Kippen", "Ringe am Objekt ziehen: um alle drei Achsen, in 15°-Schritten", true, group)
|
||||
rotate_btn.pressed.connect(func() -> void: _set_mode("rotate"))
|
||||
box.add_child(rotate_btn)
|
||||
move_btn.set_pressed_no_signal(true)
|
||||
box.add_child(VSeparator.new())
|
||||
ground_btn = _btn("⤓ Auf den Boden", "Auswahl aufs Terrain absetzen (Strg+Bild↑)", false, null)
|
||||
ground_btn.pressed.connect(func() -> void: main.interaction.snap_selection_to_floor())
|
||||
box.add_child(ground_btn)
|
||||
dup_btn = _btn("⧉ Kopie", "Duplizieren (Strg+D)", false, null)
|
||||
dup_btn.pressed.connect(func() -> void: main.interaction.duplicate_selected())
|
||||
box.add_child(dup_btn)
|
||||
delete_btn = _btn("🗑 Löschen", "Löschen (Entf) — Strg+Z holt es zurück", false, null)
|
||||
delete_btn.pressed.connect(func() -> void: main.interaction.delete_selected())
|
||||
box.add_child(delete_btn)
|
||||
|
||||
|
||||
func _btn(text: String, tip: String, toggle: bool, group: ButtonGroup) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = text
|
||||
b.tooltip_text = tip
|
||||
b.toggle_mode = toggle
|
||||
if group != null:
|
||||
b.button_group = group
|
||||
b.custom_minimum_size = UIScale.v2(0, 36)
|
||||
b.focus_mode = Control.FOCUS_NONE # Leertaste/Pfeile bleiben beim Viewport
|
||||
return b
|
||||
|
||||
|
||||
func _set_mode(m: String) -> void:
|
||||
if main.gizmo != null:
|
||||
main.gizmo.set_mode(m)
|
||||
|
||||
|
||||
## Sichtbarkeit pro Frame aus der Auswahl ableiten (wie der Gizmo selbst).
|
||||
func _process(_delta: float) -> void:
|
||||
if main == null or main.get("interaction") == null:
|
||||
return
|
||||
var inter = main.interaction
|
||||
var mode: String = str(main.get("tool_mode"))
|
||||
var want: bool = (mode == "objects" or mode == "bauen") \
|
||||
and not inter.selected_indices.is_empty() \
|
||||
and not bool(inter.get("placing")) \
|
||||
and main.get("playtest") == null
|
||||
if want != visible:
|
||||
visible = want
|
||||
if visible and main.gizmo != null:
|
||||
var rotate_on: bool = main.gizmo.mode == "rotate"
|
||||
if rotate_btn.button_pressed != rotate_on:
|
||||
rotate_btn.set_pressed_no_signal(rotate_on)
|
||||
move_btn.set_pressed_no_signal(not rotate_on)
|
||||
|
|
@ -70,12 +70,29 @@ var _drag_before: Dictionary = {} # History-Snapshot VOR dem Zug
|
|||
|
||||
var _arrows := {} # "x"/"y"/"z" → Node3D (Pfeil-Root)
|
||||
|
||||
# --- Dreh-/Kipp-Modus (25.10.): Ringe um alle drei Achsen -------------------
|
||||
const RING_R := 1.5 # Ring-Radius in Metern
|
||||
const RING_SEGMENTS := 48
|
||||
const ROT_STEP_DEG := 15.0
|
||||
var mode := "move" # "move" = Pfeile, "rotate" = Ringe
|
||||
var rot_step_deg := ROT_STEP_DEG
|
||||
var _rings := {} # "x"/"y"/"z" → MeshInstance3D
|
||||
var _rot_dragging := false
|
||||
var _rot_axis := ""
|
||||
var _rot_center := Vector2.ZERO
|
||||
var _rot_start_angle := 0.0
|
||||
var _rot_applied := 0.0 # bisher angewendete (gerastete) Grad
|
||||
var _rot_suffix := "" # merge-Kennung: EIN Undo-Schritt pro Zug
|
||||
var _rot_serial := 0
|
||||
|
||||
|
||||
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||||
main = p_main
|
||||
camera = p_camera
|
||||
view_builder = p_builder
|
||||
_build_arrows()
|
||||
_build_rings()
|
||||
_apply_mode_visibility()
|
||||
visible = false
|
||||
|
||||
|
||||
|
|
@ -119,6 +136,53 @@ func _build_arrows() -> void:
|
|||
_arrows[a] = root
|
||||
|
||||
|
||||
## Drehringe bauen: je Achse ein dünner Torus, ungeschattet, ohne Tiefentest.
|
||||
func _build_rings() -> void:
|
||||
for a in AXES:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = AXIS_COLORS[a]
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.no_depth_test = true
|
||||
mat.render_priority = 20
|
||||
var ring := MeshInstance3D.new()
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = RING_R - 0.04
|
||||
torus.outer_radius = RING_R + 0.04
|
||||
torus.rings = 48
|
||||
torus.ring_segments = 8
|
||||
ring.mesh = torus
|
||||
ring.material_override = mat
|
||||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
# Torus liegt lokal in der XZ-Ebene (Achse = Y).
|
||||
if a == "x":
|
||||
ring.rotation.z = PI / 2.0
|
||||
elif a == "z":
|
||||
ring.rotation.x = PI / 2.0
|
||||
ring.visible = false
|
||||
add_child(ring)
|
||||
_rings[a] = ring
|
||||
|
||||
|
||||
## Modus wechseln ("move" = Verschieben, "rotate" = Drehen/Kippen).
|
||||
func set_mode(m: String) -> void:
|
||||
if m != "move" and m != "rotate":
|
||||
return
|
||||
if _dragging:
|
||||
_finish_drag(false)
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
mode = m
|
||||
_apply_mode_visibility()
|
||||
|
||||
|
||||
func _apply_mode_visibility() -> void:
|
||||
for a in _arrows:
|
||||
(_arrows[a] as Node3D).visible = mode == "move"
|
||||
for a in _rings:
|
||||
(_rings[a] as Node3D).visible = mode == "rotate"
|
||||
|
||||
|
||||
func _axis_vec(a: String) -> Vector3:
|
||||
return AXES.get(a, Vector3.ZERO) as Vector3
|
||||
|
||||
|
|
@ -169,26 +233,43 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
var inter = main.get("interaction")
|
||||
if inter == null or bool(inter.get("placing")):
|
||||
return
|
||||
if mode == "rotate":
|
||||
var ring_axis := pick_ring(mb.position)
|
||||
if ring_axis != "":
|
||||
_begin_rotate(ring_axis, mb.position)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var axis := pick(mb.position)
|
||||
if axis != "":
|
||||
_begin_drag(axis, mb.position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _dragging:
|
||||
_finish_drag(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \
|
||||
or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN) and mb.pressed:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
get_viewport().set_input_as_handled() # Mausrad zoomt nicht mitten im Dreh-Zug
|
||||
elif _dragging:
|
||||
var dir := 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1
|
||||
cycle_step(dir)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _rot_dragging:
|
||||
_rotate_motion((event as InputEventMouseMotion).position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _dragging:
|
||||
_drag_motion((event as InputEventMouseMotion).position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventKey and (event as InputEventKey).pressed \
|
||||
and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _dragging:
|
||||
_finish_drag(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
|
@ -215,6 +296,35 @@ func pick(screen_pos: Vector2) -> String:
|
|||
return best
|
||||
|
||||
|
||||
## Ring unter dem Cursor (Abstand zum projizierten Kreis) — "" = keiner.
|
||||
func pick_ring(screen_pos: Vector2) -> String:
|
||||
if not visible or mode != "rotate":
|
||||
return ""
|
||||
var best := ""
|
||||
var best_d := HIT_PX
|
||||
for a in AXES:
|
||||
var pts := _ring_screen_points(a)
|
||||
for i in pts.size() - 1:
|
||||
if pts[i] == Vector2.INF or pts[i + 1] == Vector2.INF:
|
||||
continue
|
||||
var d := _dist_point_segment(screen_pos, pts[i], pts[i + 1])
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = a
|
||||
return best
|
||||
|
||||
|
||||
func _ring_screen_points(a: String) -> Array:
|
||||
var axis := _axis_vec(a)
|
||||
var u := axis.cross(Vector3.UP if absf(axis.dot(Vector3.UP)) < 0.9 else Vector3.RIGHT).normalized()
|
||||
var v := axis.cross(u).normalized()
|
||||
var out: Array = []
|
||||
for i in RING_SEGMENTS + 1:
|
||||
var t := TAU * float(i) / float(RING_SEGMENTS)
|
||||
out.append(_safe_unproject(global_position + (u * cos(t) + v * sin(t)) * RING_R))
|
||||
return out
|
||||
|
||||
|
||||
func _safe_unproject(p: Vector3) -> Vector2:
|
||||
if camera == null:
|
||||
return Vector2.INF
|
||||
|
|
@ -372,12 +482,90 @@ func _finish_drag(commit: bool) -> void:
|
|||
## Für die Esc-Kette in main.gd: Zug abbrechen, falls einer läuft. true =
|
||||
## es lief einer und wurde abgebrochen (idempotent, weil _dragging prüft).
|
||||
func cancel_drag_if_active() -> bool:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
return true
|
||||
if not _dragging:
|
||||
return false
|
||||
_finish_drag(false)
|
||||
return true
|
||||
|
||||
|
||||
# --------------------------------------------------- Drehen / Kippen -------
|
||||
|
||||
func _ring_angle(mouse: Vector2) -> float:
|
||||
var d := mouse - _rot_center
|
||||
return atan2(d.y, d.x)
|
||||
|
||||
|
||||
func _begin_rotate(axis: String, mouse: Vector2) -> void:
|
||||
var inter = main.get("interaction")
|
||||
if inter == null or inter.selected_indices.is_empty():
|
||||
return
|
||||
var c := _safe_unproject(global_position)
|
||||
if c == Vector2.INF:
|
||||
return
|
||||
_rot_axis = axis
|
||||
_rot_center = c
|
||||
_rot_start_angle = _ring_angle(mouse)
|
||||
_rot_applied = 0.0
|
||||
_rot_serial += 1
|
||||
_rot_suffix = "#g%d" % _rot_serial
|
||||
_rot_dragging = true
|
||||
var name := "Drehen (Y)" if axis == "y" else ("Kippen um X" if axis == "x" else "Kippen um Z")
|
||||
main.call("_set_status", "%s — Ring ziehen, rastet in %.0f°-Schritten; Esc bricht ab, Strg+Z nimmt den Zug zurück." % [name, rot_step_deg])
|
||||
|
||||
|
||||
func _rotate_motion(mouse: Vector2) -> void:
|
||||
# Bildschirmwinkel → Drehwinkel um die Achse (Vorzeichen je nach Blickrichtung).
|
||||
var delta_screen := wrapf(_ring_angle(mouse) - _rot_start_angle, -PI, PI)
|
||||
var facing := _axis_vec(_rot_axis).dot(camera.global_transform.basis.z)
|
||||
var deg := -rad_to_deg(delta_screen) * (1.0 if facing >= 0.0 else -1.0)
|
||||
var snapped_deg := snappedf(deg, rot_step_deg)
|
||||
_rotate_to(snapped_deg)
|
||||
|
||||
|
||||
## Auf einen Gesamtwinkel (gerastet) nachführen — in Schritten, EIN Undo-Eintrag.
|
||||
func _rotate_to(total_deg: float) -> void:
|
||||
var diff := total_deg - _rot_applied
|
||||
if absf(diff) < 0.001:
|
||||
return
|
||||
var inter = main.get("interaction")
|
||||
if inter == null:
|
||||
return
|
||||
if _rot_axis == "y":
|
||||
inter.rotate_selected(diff, _rot_suffix)
|
||||
else:
|
||||
inter.tilt_selected(_rot_axis == "x", diff, _rot_suffix)
|
||||
_rot_applied = total_deg
|
||||
|
||||
|
||||
func _finish_rotate(commit: bool) -> void:
|
||||
if not _rot_dragging:
|
||||
return
|
||||
_rot_dragging = false
|
||||
if not commit and absf(_rot_applied) > 0.001:
|
||||
main.call("_do_undo") # der ganze Zug ist EIN Eintrag → komplett zurück
|
||||
main.call("_set_status", "Dreh-Zug abgebrochen — Objekt wie vorher.")
|
||||
_rot_axis = ""
|
||||
_rot_applied = 0.0
|
||||
|
||||
|
||||
## Probes/Tests: derselbe Pfad wie ein echter Ring-Zug (ohne Maus-Mathe).
|
||||
func demo_rotate_axis(axis: String, deg: float) -> bool:
|
||||
var inter = main.get("interaction") if main != null else null
|
||||
if inter == null or inter.selected_indices.is_empty() or absf(deg) < 0.001:
|
||||
return false
|
||||
_rot_axis = axis
|
||||
_rot_serial += 1
|
||||
_rot_suffix = "#g%d" % _rot_serial
|
||||
_rot_applied = 0.0
|
||||
_rotate_to(snappedf(deg, rot_step_deg))
|
||||
_rot_axis = ""
|
||||
_rot_applied = 0.0
|
||||
return true
|
||||
|
||||
|
||||
# ------------------------------------------------------ Schrittweite -------
|
||||
|
||||
## Mausrad WÄHREND des Ziehens: durch die Voreinstellungen blättern
|
||||
|
|
|
|||
101
tests/test_ui_aufraeumen.gd
Normal file
101
tests/test_ui_aufraeumen.gd
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
extends SceneTree
|
||||
## Editor-Test (headless): UI-Aufräumen 25.10. — eigene Tabs Terrain/Malen/Bauen,
|
||||
## Viewport-Werkzeugleiste, Dreh-/Kipp-Gizmo (alle Achsen, Undo), Kontrast.
|
||||
## Ausführen: godot --headless --path . --script res://tests/test_ui_aufraeumen.gd
|
||||
|
||||
var failures := 0
|
||||
var checks := 0
|
||||
var main: Control
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
main = (load("res://scenes/main.tscn") as PackedScene).instantiate()
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
await process_frame
|
||||
(main.doc["objects"] as Array).append({
|
||||
"id": "crate_wood", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0})
|
||||
main.call("_after_structure_change")
|
||||
main.history.clear()
|
||||
await process_frame
|
||||
|
||||
print("== Tabs ==")
|
||||
var tabs: TabContainer = main.ui_right_panel
|
||||
var titel: Array = []
|
||||
for i in tabs.get_tab_count():
|
||||
titel.append(tabs.get_tab_title(i))
|
||||
_check(titel.has("Terrain") and titel.has("Malen") and titel.has("Bauen"), "Tabs Terrain, Malen, Bauen vorhanden")
|
||||
_check(main.malen_mod != null and main.bauen_mod != null, "Module malen/bauen gebaut")
|
||||
_check(main.terrain_mod.terrain_mode_paint.get_parent() == main.terrain_mod.terrain_panel and not main.terrain_mod.terrain_mode_paint.visible,
|
||||
"Mal-Modus nicht mehr als Knopf im Terrain-Tab")
|
||||
_check(main.build_edit_btn.get_parent() == main.bauen_mod, "Bauen-Knopf wohnt im Bauen-Tab")
|
||||
_check(main.malen_mod.mat_buttons.size() == SvmapIO.MATERIAL_IDS.size(), "Malen: eine Kachel je Material")
|
||||
|
||||
print("== Malen-Tab ==")
|
||||
main.call("_set_tool_mode", "terrain")
|
||||
_check(main.terrain_mod.terrain_panel.visible and not main.malen_mod.malen_panel.visible, "Terrain aktiv: nur Terrain-Panel")
|
||||
main.terrain_mod.terrain_mode_paint.button_pressed = true
|
||||
_check(main.malen_mod.malen_panel.visible and main.malen_mod.palette.visible and not main.terrain_mod.terrain_panel.visible,
|
||||
"Malen aktiv: Malen-Panel statt Terrain-Panel")
|
||||
_check(tabs.current_tab == main.malen_mod.get_index(), "Tab springt auf Malen")
|
||||
main.malen_mod.radius_slider.value = 20.0
|
||||
_check(int(main.terrain_mod.terrain_radius_slider.value) == 20, "Malen-Radius steuert den Pinsel")
|
||||
main.call("_set_tool_mode", "objects")
|
||||
|
||||
print("== Lesbarkeit ==")
|
||||
var muted: Color = main.malen_mod.theme_color("muted")
|
||||
_check(muted.a > 0.7 and muted.r > 0.9, "muted-Farbe ist hell (nicht Schwarz)")
|
||||
_check(main.status_mod.stats_objects.get_theme_color("font_color").a > 0.0, "Statuszeile lesbar")
|
||||
|
||||
print("== Werkzeugleiste + Dreh-Gizmo ==")
|
||||
_check(not main.viewport_toolbar.visible, "ohne Auswahl keine Werkzeugleiste")
|
||||
main.interaction.call("select_object", 0)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_check(main.viewport_toolbar.visible, "mit Auswahl erscheint die Werkzeugleiste")
|
||||
_check(main.gizmo.mode == "move", "Standard: Verschieben")
|
||||
main.viewport_toolbar.rotate_btn.pressed.emit()
|
||||
_check(main.gizmo.mode == "rotate", "Knopf Drehen schaltet den Gizmo um")
|
||||
var obj: Dictionary = main.doc["objects"][0]
|
||||
var n0: int = main.history.entries.size()
|
||||
_check(main.gizmo.demo_rotate_axis("x", 30.0), "Kippen um X")
|
||||
_check(main.gizmo.demo_rotate_axis("z", -15.0), "Kippen um Z")
|
||||
_check(main.gizmo.demo_rotate_axis("y", 45.0), "Drehen um Y")
|
||||
var rot := SvmapIO.objekt_rotation(obj)
|
||||
_check(is_equal_approx(rot.x, 30.0) and is_equal_approx(fposmod(rot.z, 360.0), 345.0) and is_equal_approx(rot.y, 45.0),
|
||||
"Euler (%s) = (30, 45, 345)" % str(rot))
|
||||
_check(main.history.entries.size() == n0 + 3, "jeder Zug = ein Undo-Schritt (%d neu)" % (main.history.entries.size() - n0))
|
||||
main.call("_do_undo")
|
||||
obj = main.doc["objects"][0]
|
||||
_check(is_equal_approx(SvmapIO.objekt_rotation(obj).y, 0.0), "Undo nimmt den Y-Zug zurück")
|
||||
main.call("_do_undo")
|
||||
main.call("_do_undo")
|
||||
obj = main.doc["objects"][0]
|
||||
_check(SvmapIO.objekt_rotation(obj).is_equal_approx(Vector3.ZERO), "dreimal Undo = Ausgangslage")
|
||||
main.viewport_toolbar.move_btn.pressed.emit()
|
||||
_check(main.gizmo.mode == "move", "Verschieben schaltet zurück")
|
||||
var scale_btns := 0
|
||||
for c in main.viewport_toolbar.find_children("*", "Button", true, false):
|
||||
if str((c as Button).text).to_lower().contains("skal"):
|
||||
scale_btns += 1
|
||||
_check(scale_btns == 0, "kein Skalieren-Werkzeug (Objekte behalten ihre Größe)")
|
||||
|
||||
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: ", label)
|
||||
else:
|
||||
failures += 1
|
||||
print(" FAIL: ", label)
|
||||
Loading…
Reference in a new issue