scopedvr-map-editor/src/viewport/gizmo.gd
2026-10-31 15:37:09 +01:00

624 lines
21 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Node3D
## Achsen-Gizmo (Karte t_e0120dc2): drei farbige Pfeile (X rot, Y grün,
## Z blau) am EINZELN markierten Objekt. Zieht man einen Pfeil, bewegt sich
## das Objekt GENAU entlang dieser Achse — die Zahlen im Objekt-Inspektor
## laufen LIVE mit. Mausrad WÄHREND des Ziehens wechselt die Schrittweite
## (fein/normal/grob); derselbe Wert steht als „Schrittweite“-Feld im
## rechten Panel (main.gd).
##
## Regeln der Karte:
## - NUR Einzelauswahl (Mehrfachauswahl bekommt kein Gizmo — Folgekarte).
## - JEDER Zug = EIN Undo-Schritt (Commit beim Loslassen). Esc bricht EXAKT
## auf die Startposition zurück (byte-identisch, wie grab_cancel).
## - Zieh-Werte rasten auf Vielfache der Schrittweite (Voreinstellungen
## 0,1 / 0,5 / 2 m; das Feld im Panel nimmt jeden Wert 0,01–10 m).
## - Klemmen wie der Inspektor: X/Z an der Map-Grenze, Y nie unter dem
## Terrain (X/Z-Zug folgt dem Boden wie der normale Objekt-Zug) und
## oben an der Höhen-Grenze.
##
## Eingabe-Verdrahtung: dieses Gizmo hängt als KIND an interaction.gd —
## Knoten bekommen _unhandled_input TIEFER IM BAUM ZUERST, so fängt es
## Pfeil-Klicks ab, BEVOR die Auswahl-/Zieh-Logik des Parents sie sieht.
## interaction.gd bleibt dadurch UNVERÄNDERT (Karten-Grenze) und der
## bestehende Klick-/Zieh-Pfad wird 1:1 wiederverwendet.
const SvmapDocument := preload("res://core/svmap/map_document.gd")
const AXIS_LEN := 2.2 # Pfeil-Länge in Metern (Weltmaß)
const SHAFT_R := 0.045
const CONE_H := 0.45
const CONE_R := 0.13
const HIT_PX := 18.0 # Treffer-Toleranz am Bildschirm (Pixel)
const MIN_PX_PER_M := 10.0 # Zieh-Sensitivität deckeln (Achse fast im Blick)
const STEP_MIN := 0.01
const STEP_MAX := 10.0
const STEP_PRESETS := [0.1, 0.5, 2.0]
const STEP_PRESET_NAMES := ["fein", "normal", "grob"]
const AXES := {
"x": Vector3(1.0, 0.0, 0.0),
"y": Vector3(0.0, 1.0, 0.0),
"z": Vector3(0.0, 0.0, 1.0),
}
const AXIS_COLORS := {
"x": Color(1.0, 0.25, 0.25),
"y": Color(0.3, 0.95, 0.35),
"z": Color(0.35, 0.55, 1.0),
}
var main # src/ui/main.gd (script-erweitert)
var camera: Camera3D
var view_builder # src/viewport/map_view_builder.gd
var step_m := 0.5 # Schrittweite (m) — Panel-Feld + Mausrad-Presets
# --- Zieh-Zustand ---
var _dragging := false
var _drag_idx := -1
var _drag_axis := ""
var _drag_axis_vec := Vector3.ZERO
var _press_screen := Vector2.ZERO
var _screen_axis := Vector2.RIGHT # Achse am Bildschirm (normalisiert, px)
var _px_per_m := 1.0
var _start_pos := Vector3.ZERO # doc-Position beim Zieh-Start
var _start_saved: Array = [] # exakte Start-Position für Esc-Rückabwicklung
var _drag_lift := 0.0 # Boden-Abstand (X/Z-Zug folgt dem Terrain)
var _drag_moved := false
var _last_delta := 0.0 # zuletzt angewendeter Delta (m) — 0 = kein Commit
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
## Pfeile bauen: Schaft (Zylinder) + Spitze (Kegel), ungeschattet und OHNE
## Tiefentest — die Pfeile bleiben auch vor/über Objekten gut sichtbar.
func _build_arrows() -> void:
for a in AXES:
var root := Node3D.new()
# Alle Pfeile zeigen lokal +Y; der Root dreht sie auf die Welt-Achse.
if a == "x":
root.rotation.z = -PI / 2.0 # +Y → +X
elif a == "z":
root.rotation.x = PI / 2.0 # +Y → +Z
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 shaft := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = SHAFT_R
cyl.bottom_radius = SHAFT_R
cyl.height = AXIS_LEN
shaft.mesh = cyl
shaft.material_override = mat
shaft.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
shaft.position.y = AXIS_LEN * 0.5
root.add_child(shaft)
var tip := MeshInstance3D.new()
var cone := CylinderMesh.new()
cone.top_radius = 0.0
cone.bottom_radius = CONE_R
cone.height = CONE_H
tip.mesh = cone
tip.material_override = mat
tip.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
tip.position.y = AXIS_LEN + CONE_H * 0.5
root.add_child(tip)
add_child(root)
_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.06
torus.outer_radius = RING_R + 0.06
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
var inter = main.get("interaction") if main != null else null
if inter != null:
inter.no_drag_move = (m == "rotate")
_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
## Sichtbarkeit + Position JEDES Frames aus der Auswahl ableiten (kein
## Verdrahten jedes Auswahl-Wegs nötig — Auswahl kommt aus interaction.gd,
## Baum, Undo/Redo, Inspektor … alles landet automatisch hier).
func _process(_delta: float) -> void:
if main == null or view_builder == null:
return
var inter = main.get("interaction")
if inter == null:
return
var sel: Array = inter.selected_indices
if sel.size() != 1:
visible = false
return
if bool(inter.get("placing")):
visible = false # Ghost-Platzieren: keine Pfeile im Weg
return
var idx: int = int(inter.selected_index)
var markers: Array = view_builder.object_markers
if idx < 0 or idx >= markers.size():
visible = false
return
var marker = markers[idx]
if marker == null or not is_instance_valid(marker):
visible = false
return
visible = true
global_position = marker.global_position
func _unhandled_input(event: InputEvent) -> void:
if main == null or camera == null:
return
var doc: Dictionary = main.get("doc")
if doc == null or doc.is_empty():
return
var tool_mode: String = str(main.get("tool_mode"))
if tool_mode == "terrain" or tool_mode == "spawns":
return # Terrain-Pinsel / Spawn-Zonen haben das Zepter
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
if _dragging:
return
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 _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 _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 _rot_dragging:
_finish_rotate(false)
get_viewport().set_input_as_handled()
elif _dragging:
_finish_drag(false)
get_viewport().set_input_as_handled()
# ------------------------------------------------------------- Treffer -----
## Achse unter dem Cursor (Bildschirm-Segment-Distanz) — "" = kein Pfeil.
func pick(screen_pos: Vector2) -> String:
if not visible:
return ""
var o := _safe_unproject(global_position)
if o == Vector2.INF:
return ""
var best := ""
var best_d := HIT_PX
for a in AXES:
var t := _safe_unproject(global_position + _axis_vec(a) * AXIS_LEN)
if t == Vector2.INF:
continue
var d := _dist_point_segment(screen_pos, o, t)
if d < best_d:
best_d = d
best = a
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 * 1.6
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
if camera.is_position_behind(p):
return Vector2.INF
return camera.unproject_position(p)
func _dist_point_segment(p: Vector2, a: Vector2, b: Vector2) -> float:
var ab := b - a
var len_sq := ab.length_squared()
if len_sq < 0.0001:
return p.distance_to(a)
var t := clampf((p - a).dot(ab) / len_sq, 0.0, 1.0)
return p.distance_to(a + ab * t)
# -------------------------------------------------------------- Ziehen -----
func _begin_drag(axis: String, mb_pos: Vector2) -> void:
if not _begin_drag_state(axis):
return
# Bildschirm-Mathe: Achse in Pixeln → Zieh-Sensitivität (Meter pro Pixel).
var markers: Array = view_builder.object_markers
var marker = markers[_drag_idx]
var origin: Vector3 = marker.global_position
var o := _safe_unproject(origin)
var t := _safe_unproject(origin + _drag_axis_vec * AXIS_LEN)
if o == Vector2.INF or t == Vector2.INF:
_dragging = false
return
var seg := t - o
var seg_len := seg.length()
if seg_len < 1.0:
_dragging = false
return
_screen_axis = seg.normalized()
_px_per_m = maxf(seg_len / AXIS_LEN, MIN_PX_PER_M)
_press_screen = mb_pos
main.call("_set_status", "Gizmo-Zug: entlang %s ziehen — Mausrad wechselt die Schrittweite (%.2f m), Esc bricht ab." % [
_drag_axis.to_upper(), step_m])
## Zieh-Zustand NUR aus dem Doc/Marke r aufsetzen (ohne Bildschirm-Mathe —
## auch für den Demo-/Test-Hook demo_drag_axis).
func _begin_drag_state(axis: String) -> bool:
var inter = main.get("interaction")
if inter == null or inter.selected_indices.size() != 1:
return false
var idx: int = int(inter.selected_index)
var markers: Array = view_builder.object_markers
if idx < 0 or idx >= markers.size():
return false
var marker = markers[idx]
if marker == null or not is_instance_valid(marker):
return false
var objects: Array = main.doc.get("objects", [])
if idx >= objects.size():
return false
var pos: Array = (objects[idx] as Dictionary).get("pos", [0.0, 0.0, 0.0])
_drag_idx = idx
_drag_axis = axis
_drag_axis_vec = _axis_vec(axis)
_press_screen = Vector2.ZERO
_start_pos = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
_start_saved = (pos as Array).duplicate()
_drag_lift = marker.position.y - SvmapDocument.terrain_height_at(
main.doc, _start_pos.x, _start_pos.z)
_drag_before = main.history.snapshot(["objects"])
_drag_moved = false
_last_delta = 0.0
_dragging = true
return true
func _drag_motion(mb_pos: Vector2) -> void:
var d := mb_pos - _press_screen
var raw := d.dot(_screen_axis) / _px_per_m
_apply_axis_delta(snappedf(raw, step_m))
## Delta entlang der Achse anwenden: doc + Marke + Gizmo, Inspektor LIVE.
func _apply_axis_delta(delta: float) -> void:
if not _dragging or absf(delta - _last_delta) < 0.0005:
return
var objects: Array = main.doc.get("objects", [])
if _drag_idx >= objects.size():
_finish_drag(false)
return
var np := _start_pos + _drag_axis_vec * delta
var half := _terrain_half()
np.x = clampf(np.x, -half.x, half.x)
np.z = clampf(np.z, -half.y, half.y)
if _drag_axis == "y":
np.y = clampf(np.y, SvmapDocument.terrain_height_at(main.doc, np.x, np.z), _max_y())
else:
# X/Z-Zug folgt dem Boden (gleiches Verhalten wie der normale Zug):
# der Boden-Abstand bleibt erhalten, nichts begräbt sich.
np.y = SvmapDocument.terrain_height_at(main.doc, np.x, np.z) + _drag_lift
var obj: Dictionary = objects[_drag_idx]
obj["pos"] = [np.x, np.y, np.z]
var markers: Array = view_builder.object_markers
if _drag_idx < markers.size():
var marker = markers[_drag_idx]
if marker != null and is_instance_valid(marker):
marker.position = np
global_position = np
_last_delta = delta
_drag_moved = true
main.call("_sync_selection_panel") # Inspektor-Zahlen LIVE mitziehen
## Zug beenden: commit = true → EIN Undo-Schritt (wirklich bewegt),
## commit = false → exakte Rückabwicklung, KEIN History-Eintrag.
func _finish_drag(commit: bool) -> void:
if not _dragging:
return
_dragging = false
var objects: Array = main.doc.get("objects", [])
var idx := _drag_idx
if commit and absf(_last_delta) > 0.0005 and idx >= 0 and idx < objects.size():
var desc: String
match _drag_axis:
"x":
desc = "Objekt entlang X verschoben (Gizmo)"
"y":
desc = "Objekt angehoben/abgesenkt (Gizmo, Y-Achse)"
_:
desc = "Objekt entlang Z verschoben (Gizmo)"
main.history.commit(desc, _drag_before, main.history.snapshot(["objects"]))
main.set("dirty", true)
main.call("_refresh_file_label")
main.call("_refresh_history_buttons")
main.call("_refresh_stats")
main.call("_sync_selection_panel")
main.call("_set_status", "%s — %.2f m. Strg+Z nimmt den Zug zurück; Mausrad WÄHREND des Ziehens wechselt fein/grob." % [
desc, absf(_last_delta)])
elif not commit and idx >= 0 and idx < objects.size():
(objects[idx] as Dictionary)["pos"] = _start_saved.duplicate()
var markers: Array = view_builder.object_markers
if idx < markers.size():
var marker = markers[idx]
if marker != null and is_instance_valid(marker):
marker.position = _start_pos
global_position = _start_pos
main.call("_sync_selection_panel")
main.call("_set_status", "Gizmo-Zug abgebrochen — Position wie vorher.")
_drag_before = {}
_drag_idx = -1
_drag_axis = ""
_drag_moved = false
_last_delta = 0.0
## 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
## (fein 0,1 → normal 0,5 → grob 2,0 m, wrappt). Das Panel-Feld folgt.
func cycle_step(dir: int) -> void:
var idx := STEP_PRESETS.find(step_m)
if idx < 0:
idx = STEP_PRESETS.size() - 1 # eigener Feld-Wert: vom größten Preset starten
idx = wrapi(idx + dir, 0, STEP_PRESETS.size())
set_step_m(STEP_PRESETS[idx])
main.call("_set_status", "Gizmo-Schrittweite: %.2f m (%s) — Mausrad wechselt weiter, das Feld rechts setzt sie exakt." % [
step_m, STEP_PRESET_NAMES[idx]])
## Feld-Wert übernehmen (Panel-Feld ODER Mausrad-Preset). Haupt-Var hält den
## Wert über UI-Rebuilds (UIScale) hinweg.
func set_step_m(v: float) -> void:
step_m = clampf(v, STEP_MIN, STEP_MAX)
if main != null:
main.set("gizmo_step_m", step_m)
# ------------------------------------------------------- Demo-/Test-Hook ---
## Probes/Tests: denselben Anwendungs- + Commit-Pfad wie ein echter Zug,
## ohne Bildschirm-Mathe. true = es wurde wirklich bewegt + committed.
func demo_drag_axis(axis: String, meters: float) -> bool:
if main == null:
return false
var applied := snappedf(meters, step_m)
if absf(applied) < 0.0005:
return false
if not _begin_drag_state(axis):
return false
_apply_axis_delta(applied)
_finish_drag(true)
return true
# ------------------------------------------------------------ Helfer -------
func _terrain_half() -> Vector2:
var terrain: Dictionary = main.doc.get("terrain", {})
return Vector2(float(terrain.get("size_x_m", 1024.0)),
float(terrain.get("size_z_m", 1024.0))) * 0.5
func _max_y() -> float:
var bounds: Dictionary = main.doc.get("bounds", {})
var mx = bounds.get("max", null)
if mx is Array and (mx as Array).size() == 3:
return float((mx as Array)[1])
return 80.0