extends Control signal calibration_finished(calibration: AccelCalibration) enum Phase { WAITING, NEUTRAL, RIGHT, LEFT, DONE } const TRAVEL_PX := 140.0 const NEUTRAL_DURATION := 3.5 const SWAY_DURATION := 5.0 const SAMPLE_START_FRACTION := 0.5 const CIRCLE_RADIUS := 44.0 const NEEDLE_LEN := 58.0 var _phase := Phase.WAITING var _phase_time := 0.0 var _guide_offset := Vector2.ZERO var _instruction := "" var _calibration := AccelCalibration.new() var _connection_notified := false var _context_label := "" var _last_accel := Vector3i.ZERO @onready var _hint_label: Label = $HintLabel @onready var _start_button: Button = $StartButton func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) mouse_filter = Control.MOUSE_FILTER_PASS _start_button.pressed.connect(_on_start_pressed) _show_waiting() func set_context_label(text: String) -> void: _context_label = text if _phase == Phase.WAITING: _show_waiting() func start() -> void: _calibration.reset() _connection_notified = false _phase = Phase.WAITING _phase_time = 0.0 _guide_offset = Vector2.ZERO _last_accel = Vector3i.ZERO visible = true _start_button.visible = true _show_waiting() queue_redraw() func is_active() -> bool: return _phase != Phase.DONE func feed_accel(accel: Vector3i) -> void: _last_accel = accel if not _should_record(): return match _phase: Phase.NEUTRAL: _calibration.record_neutral(accel) Phase.RIGHT: _calibration.record_right(accel) Phase.LEFT: _calibration.record_left(accel) func get_live_projection() -> float: if _calibration.uses_vector: return _calibration.project(_last_accel) - _calibration.center return 0.0 func notify_connected() -> void: if _phase != Phase.WAITING or _connection_notified: return _hint_label.text = "Verbunden – tippe „Kalibrierung starten“ oder warte…" begin_calibration() func begin_calibration() -> void: if _phase == Phase.NEUTRAL or _phase == Phase.RIGHT or _phase == Phase.LEFT: return _connection_notified = true _start_button.visible = false _begin_phase(Phase.NEUTRAL) func _on_start_pressed() -> void: begin_calibration() func _process(delta: float) -> void: if _phase == Phase.WAITING or _phase == Phase.DONE: return _phase_time += delta var duration := _phase_duration() var t := clampf(_phase_time / duration, 0.0, 1.0) var ease := t * t * (3.0 - 2.0 * t) match _phase: Phase.NEUTRAL: _guide_offset = Vector2.ZERO _instruction = _phase_instruction( "Halte den Zylinder senkrecht und ruhig\n(mittlere Position – nicht schwanken)" ) if _phase_time >= duration: _begin_phase(Phase.RIGHT) Phase.RIGHT: _guide_offset.x = TRAVEL_PX * ease _instruction = _phase_instruction( "Schwank den Pod langsam nach rechts ↷\nWie ein Pendel – Spitze kippt nach rechts" ) if _phase_time >= duration: _begin_phase(Phase.LEFT) Phase.LEFT: _guide_offset.x = -TRAVEL_PX * ease _instruction = _phase_instruction( "Schwank den Pod langsam nach links ↶\nWie ein Pendel – Spitze kippt nach links" ) if _phase_time >= duration: _finish() var record_hint := "" if _phase != Phase.NEUTRAL and _phase_time < duration * SAMPLE_START_FRACTION: record_hint = "\n\n… halte gleich die Endposition" elif _phase != Phase.WAITING and _phase != Phase.DONE: record_hint = "\n\n✓ Aufnahme läuft" _hint_label.text = _instruction + record_hint queue_redraw() func _phase_duration() -> float: match _phase: Phase.NEUTRAL: return NEUTRAL_DURATION return SWAY_DURATION func _should_record() -> bool: if _phase == Phase.WAITING or _phase == Phase.DONE: return false return _phase_time >= _phase_duration() * SAMPLE_START_FRACTION func _begin_phase(phase: Phase) -> void: _phase = phase _phase_time = 0.0 _guide_offset = Vector2.ZERO match phase: Phase.NEUTRAL: _instruction = _phase_instruction( "Halte den Zylinder senkrecht und ruhig\n(mittlere Position – nicht schwanken)" ) Phase.RIGHT: _instruction = _phase_instruction( "Schwank den Pod langsam nach rechts ↷\nWie ein Pendel – Spitze kippt nach rechts" ) Phase.LEFT: _instruction = _phase_instruction( "Schwank den Pod langsam nach links ↶\nWie ein Pendel – Spitze kippt nach links" ) _hint_label.text = _instruction queue_redraw() func _finish() -> void: _phase = Phase.DONE visible = false if _calibration.analyze(): calibration_finished.emit(_calibration.duplicate()) else: _hint_label.text = ( "Kalibrierung fehlgeschlagen.\n" + "Schwank weiter links/rechts oder halte die Neutralposition länger still." ) visible = true _start_button.visible = true _show_waiting() func _phase_instruction(base: String) -> String: if _context_label == "": return base return "%s\n\n%s" % [_context_label, base] func _show_waiting() -> void: _instruction = "Warte auf Verbindung zu localhost:8081…" var prefix := "%s\n\n" % _context_label if _context_label != "" else "" _hint_label.text = prefix + _instruction + "\n(Oder „Kalibrierung starten“ ohne Verbindung)" func _screen_center() -> Vector2: return get_viewport_rect().size * 0.5 func _draw() -> void: if _phase == Phase.DONE: return var pos := _screen_center() + _guide_offset var center := _screen_center() if _phase == Phase.NEUTRAL: var pulse := 0.85 + 0.15 * sin(_phase_time * 4.0) draw_circle(center, CIRCLE_RADIUS * pulse, Color(0.35, 0.85, 0.55, 0.18)) draw_arc(center, CIRCLE_RADIUS * pulse, 0.0, TAU, 64, Color(0.45, 0.95, 0.65), 2.5) draw_line(center + Vector2(-18, 0), center + Vector2(18, 0), Color(1, 1, 1, 0.35), 2.0) return draw_circle(pos, CIRCLE_RADIUS, Color(0.25, 0.72, 0.95, 0.2)) draw_arc(pos, CIRCLE_RADIUS, 0.0, TAU, 64, Color(0.35, 0.82, 1.0), 2.5) var needle_tip := pos + Vector2(0.0, -NEEDLE_LEN) draw_line(pos, needle_tip, Color(1.0, 1.0, 1.0, 0.95), 3.0) draw_circle(needle_tip, 5.0, Color(1.0, 1.0, 1.0)) if _phase == Phase.RIGHT: draw_line(center, center + Vector2(TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0) elif _phase == Phase.LEFT: draw_line(center, center + Vector2(-TRAVEL_PX, 0.0), Color(1.0, 1.0, 1.0, 0.12), 1.0)