388 lines
No EOL
14 KiB
GDScript
388 lines
No EOL
14 KiB
GDScript
extends Node3D
|
|
## Maus-Interaktion im Editor-Viewport (M2 UX-Fundament, Karte t_40d855d0):
|
|
## - Ghost-Preview: beim Platzieren schwebt eine transparente Vorschau am
|
|
## Cursor, Klick setzt das Objekt (kein "überraschtes" Ergebnis).
|
|
## - Auswahl: angeklickte Objekte färben sich orange, Ziehen verschiebt,
|
|
## R-Taste ODER Mausrad dreht (15°-Raster), Entf löscht.
|
|
## Alle Meldungen laufen über main._set_status — Klartext + nächsten Schritt
|
|
## vorschlagen (Chef-Grundsatz, map-editor-README).
|
|
##
|
|
## Lebt im selben SubViewport-Welt-Knoten wie Kamera + Viewport-Bauer;
|
|
## main.gd verdrahtet Referenzen per setup().
|
|
|
|
const ROT_STEP_DEG := 15.0
|
|
const DRAG_THRESHOLD_PX := 4.0
|
|
const GRID_M := 1.0 # Raster-Snap-Weite (Objekt-Werkzeuge links, Karte t_3a998ae4)
|
|
|
|
var main # Hauptfenster (script-erweitert, Variant — Referenzen per setup())
|
|
var camera: Camera3D
|
|
var view_builder # script-erweitert, Variant
|
|
|
|
# Platzier-Modus
|
|
var placing := false
|
|
var place_oid := ""
|
|
var place_rot_deg := 0.0
|
|
var _ghost: MeshInstance3D
|
|
var _ghost_active := false # Ghost sichtbar weil Cursor über der Map ist
|
|
|
|
# Auswahl + Greifen
|
|
var selected_index := -1
|
|
var _dragging := false
|
|
var _drag_moved := false
|
|
var _press_pos := Vector2.ZERO
|
|
var _grab_offset := Vector3.ZERO
|
|
|
|
|
|
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
|
main = p_main
|
|
camera = p_camera
|
|
view_builder = p_builder
|
|
|
|
|
|
## Kamera fragt: dreht das Mausrad gerade ein Objekt/eine Vorschau
|
|
## (dann zoomt das Rad NICHT)?
|
|
func wheel_blocked() -> bool:
|
|
return placing or selected_index >= 0
|
|
|
|
|
|
func _ground_point(viewport_pos: Vector2) -> Variant:
|
|
# Cursor-Strahl → Punkt auf der Boden-Referenzebene (Terrain-Basis).
|
|
var terrain: Dictionary = main.doc.get("terrain", {})
|
|
if terrain.is_empty():
|
|
return null
|
|
var plane_y := float(terrain.get("min_height_m", 0.0))
|
|
var origin := camera.project_ray_origin(viewport_pos)
|
|
var dir := camera.project_ray_normal(viewport_pos)
|
|
if absf(dir.y) < 0.0001:
|
|
return null
|
|
var t := (plane_y - origin.y) / dir.y
|
|
if t <= 0.0:
|
|
return null
|
|
return origin + dir * t
|
|
|
|
|
|
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
|
|
|
|
|
|
## Cursor-Strahl → getroffenes Objekt (Kugel-Approximation der Marker).
|
|
## Rückgabe: Index in doc.objects oder -1.
|
|
func _pick_object(viewport_pos: Vector2) -> int:
|
|
var origin := camera.project_ray_origin(viewport_pos)
|
|
var dir := camera.project_ray_normal(viewport_pos)
|
|
var ground: Variant = _ground_point(viewport_pos)
|
|
var ground_t := INF
|
|
if ground != null:
|
|
ground_t = (ground as Vector3).distance_to(origin)
|
|
var best := -1
|
|
var best_t := INF
|
|
for marker in view_builder.object_markers:
|
|
if marker == null or not is_instance_valid(marker):
|
|
continue
|
|
var center: Vector3 = marker.global_position + Vector3.UP * float(marker.get_meta("half_height", 1.0))
|
|
var to_center := center - origin
|
|
var tca := to_center.dot(dir)
|
|
if tca <= 0.0:
|
|
continue
|
|
var d2 := to_center.length_squared() - tca * tca
|
|
var radius := float(marker.get_meta("pick_radius", 1.0))
|
|
if d2 > radius * radius:
|
|
continue
|
|
var t := tca - sqrt(radius * radius - d2)
|
|
if t < best_t and t < ground_t + 0.01:
|
|
best = int(marker.get_meta("obj_index", -1))
|
|
best_t = t
|
|
return best
|
|
|
|
|
|
# ------------------------------------------------------------ Platzieren ----
|
|
|
|
func start_placement(oid: String) -> void:
|
|
place_oid = oid
|
|
placing = true
|
|
place_rot_deg = 0.0
|
|
_ghost_active = false
|
|
_ensure_ghost()
|
|
_ensure_ghost_visible()
|
|
var label: String = main.object_label(oid)
|
|
main.call("_set_status", "%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % label)
|
|
main.call("_highlight_library", oid)
|
|
|
|
|
|
func cancel_placement() -> void:
|
|
if _ghost != null and is_instance_valid(_ghost):
|
|
_ghost.visible = false
|
|
placing = false
|
|
place_oid = ""
|
|
_ghost_active = false
|
|
main.call("_highlight_library", "")
|
|
main.call("_base_tool_status")
|
|
|
|
|
|
func _ensure_ghost() -> void:
|
|
if _ghost != null and is_instance_valid(_ghost):
|
|
return
|
|
_ghost = MeshInstance3D.new()
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.6, 0.9, 1.0, 0.45)
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.emission_enabled = true
|
|
mat.emission = Color(0.2, 0.5, 0.8) * 0.4
|
|
_ghost.material_override = mat
|
|
add_child(_ghost)
|
|
_ghost.visible = false
|
|
|
|
|
|
func _ghost_refresh_mesh() -> void:
|
|
var lib: Dictionary = main.manifest.get("objects", {})
|
|
var entry: Dictionary = lib.get(place_oid, {})
|
|
var col: Dictionary = entry.get("collision", {})
|
|
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
|
_ghost.mesh = mesh
|
|
_ghost.position.y = MeshShapes.half_height(mesh)
|
|
|
|
|
|
func _ensure_ghost_visible() -> void:
|
|
_ghost_refresh_mesh()
|
|
_ghost.rotation.y = deg_to_rad(place_rot_deg)
|
|
_ghost.visible = true
|
|
|
|
|
|
## Raster-Snap (wenn in main angeschaltet): X/Z auf ganze Meter rasten —
|
|
## Objekte passen dann nebeneinander wie Bauklötze. Höhe bleibt Terrain.
|
|
func _apply_grid(pos: Vector3) -> Vector3:
|
|
if not bool(main.get("grid_snap")):
|
|
return pos
|
|
pos.x = ObjectOps.snap_coord(pos.x, true, GRID_M)
|
|
pos.z = ObjectOps.snap_coord(pos.z, true, GRID_M)
|
|
return pos
|
|
|
|
|
|
func _ghost_update(viewport_pos: Vector2) -> void:
|
|
var p: Variant = _ground_point(viewport_pos)
|
|
if p == null:
|
|
_ghost_active = false
|
|
_ghost.visible = false
|
|
return
|
|
_ghost_active = true
|
|
var pos := _apply_grid(p as Vector3)
|
|
var half := _terrain_half()
|
|
pos.x = clampf(pos.x, -half.x, half.x)
|
|
pos.z = clampf(pos.z, -half.y, half.y)
|
|
pos.y = SvmapDocument.terrain_height_at(main.doc, pos.x, pos.z)
|
|
_ghost.position = pos
|
|
_ghost.visible = true
|
|
|
|
|
|
func _place_object(viewport_pos: Vector2) -> void:
|
|
var p: Variant = _ground_point(viewport_pos)
|
|
if p == null:
|
|
main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true)
|
|
return
|
|
var pos: Vector3 = _apply_grid(p as Vector3)
|
|
var half := _terrain_half()
|
|
if absf(pos.x) > half.x or absf(pos.z) > half.y:
|
|
main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true)
|
|
return
|
|
|
|
# Poly-Budget prüfen, BEVOR etwas passiert (hilfreiche Meldung statt Fehler).
|
|
var lib: Dictionary = main.manifest.get("objects", {})
|
|
var entry: Dictionary = lib.get(place_oid, {})
|
|
var poly := float(entry.get("poly", 0))
|
|
var used := SvmapDocument.poly_usage(main.doc, main.manifest)
|
|
var budget := float(SvmapIO.MAX_TOTAL_POLY)
|
|
if used + poly > budget:
|
|
main.call("_set_status",
|
|
"Kein Platz mehr im Poly-Budget (%s von %s belegt) — lass ein Objekt weg oder drück Strg+Z, um etwas zurückzunehmen."
|
|
% [main._fmt_int(int(used)), main._fmt_int(int(budget))], true)
|
|
return
|
|
|
|
var before: Dictionary = main.history.snapshot(["objects"])
|
|
var pos_f := [float(pos.x), float(SvmapDocument.terrain_height_at(main.doc, pos.x, pos.z)), float(pos.z)]
|
|
(main.doc["objects"] as Array).append({
|
|
"id": place_oid,
|
|
"pos": pos_f,
|
|
"scale": 1.0,
|
|
"rot_y_deg": float(place_rot_deg),
|
|
})
|
|
main.history.commit("%s platzieren" % main.object_label(place_oid),
|
|
before, main.history.snapshot(["objects"]))
|
|
var new_index: int = (main.doc["objects"] as Array).size() - 1
|
|
main.call("_after_structure_change")
|
|
main.call("_set_status", "%s platziert. Tipp: anklicken zum Verschieben (Ziehen) und Drehen (R oder Mausrad) — Entf löscht es." % main.object_label(place_oid))
|
|
# Platzier-Modus bleibt aktiv (Mehrfach-Platzieren ohne erneutes Klicken),
|
|
# Auswahl auf das frische Objekt legen? Nein — Ghost bleibt Vorrang; ESC/Rechtsklick beendet.
|
|
main.call("_mark_tree_selection", new_index)
|
|
|
|
|
|
# --------------------------------------------------------------- Auswahl ----
|
|
|
|
func select_object(idx: int, from_tree := false) -> void:
|
|
selected_index = idx
|
|
view_builder.call("set_selected", idx)
|
|
if not from_tree:
|
|
main.call("_mark_tree_selection", idx)
|
|
main.call("_sync_selection_panel")
|
|
main.call("_selection_status", idx)
|
|
|
|
|
|
func clear_selection(silent := false) -> void:
|
|
if selected_index != -1:
|
|
selected_index = -1
|
|
view_builder.call("set_selected", -1)
|
|
if not silent:
|
|
main.call("_set_status", "Auswahl aufgehoben.")
|
|
main.call("_sync_selection_panel")
|
|
|
|
|
|
func rotate_selected(delta_deg: float) -> void:
|
|
if selected_index < 0:
|
|
return
|
|
var objects: Array = main.doc.get("objects", [])
|
|
if selected_index >= objects.size():
|
|
return
|
|
var before: Dictionary = main.history.snapshot(["objects"])
|
|
var obj: Dictionary = objects[selected_index]
|
|
var marker: Node3D = view_builder.object_markers[selected_index] if selected_index < view_builder.object_markers.size() else null
|
|
var new_rot := fmod(float(obj.get("rot_y_deg", 0.0)) + delta_deg + 3600.0, 360.0)
|
|
obj["rot_y_deg"] = float(new_rot)
|
|
if marker != null and is_instance_valid(marker):
|
|
marker.rotation.y = deg_to_rad(new_rot)
|
|
main.history.commit("Objekt gedreht", before, main.history.snapshot(["objects"]),
|
|
"rot:%d" % selected_index)
|
|
main.call("_refresh_history_buttons")
|
|
main.call("_refresh_stats")
|
|
|
|
|
|
func delete_selected() -> void:
|
|
if selected_index < 0:
|
|
main.call("_set_status", "Nichts ausgewählt — klicke zuerst ein Objekt an, das gelöscht werden soll.", true)
|
|
return
|
|
var objects: Array = main.doc.get("objects", [])
|
|
if selected_index >= objects.size():
|
|
clear_selection(true)
|
|
return
|
|
var before: Dictionary = main.history.snapshot(["objects"])
|
|
var label: String = main.object_label(str(objects[selected_index].get("id", "")))
|
|
objects.remove_at(selected_index)
|
|
main.history.commit("%s löschen" % label, before, main.history.snapshot(["objects"]))
|
|
clear_selection(true)
|
|
main.call("_after_structure_change")
|
|
main.call("_set_status", "%s gelöscht — Strg+Z (Rückgängig) holt es zurück." % label)
|
|
|
|
|
|
# ------------------------------------------------------------ Eingaben ------
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
var mode: String = main.get("tool_mode")
|
|
if mode == "terrain" or mode == "spawns":
|
|
return # Terrain-Pinsel / Spawn-Zonen haben das Zepter (eigene Werkzeuge)
|
|
if event is InputEventMouseMotion:
|
|
var mm := event as InputEventMouseMotion
|
|
if placing:
|
|
_ghost_update(mm.position)
|
|
if _dragging and not placing and selected_index >= 0:
|
|
var p: Variant = _ground_point(mm.position)
|
|
if p == null:
|
|
return
|
|
var new_pos: Vector3 = _apply_grid((p as Vector3) + _grab_offset)
|
|
var half := _terrain_half()
|
|
new_pos.x = clampf(new_pos.x, -half.x, half.x)
|
|
new_pos.z = clampf(new_pos.z, -half.y, half.y)
|
|
new_pos.y = SvmapDocument.terrain_height_at(main.doc, new_pos.x, new_pos.z)
|
|
var dist := Vector2(mm.position.x - _press_pos.x, mm.position.y - _press_pos.y).length()
|
|
if dist > DRAG_THRESHOLD_PX:
|
|
_drag_moved = true
|
|
if _drag_moved:
|
|
var marker: Node3D = view_builder.object_markers[selected_index]
|
|
if marker != null and is_instance_valid(marker):
|
|
marker.position = new_pos
|
|
return
|
|
if event is InputEventMouseButton:
|
|
var mb := event as InputEventMouseButton
|
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
|
_on_left_press(mb)
|
|
get_viewport().set_input_as_handled()
|
|
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
|
|
_on_left_release(mb)
|
|
elif mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed:
|
|
if placing:
|
|
cancel_placement()
|
|
main.call("_set_status", "Platzieren abgebrochen.")
|
|
get_viewport().set_input_as_handled()
|
|
elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
|
if placing:
|
|
place_rot_deg = fmod(place_rot_deg + ROT_STEP_DEG + 3600.0, 360.0)
|
|
_ghost.rotation.y = deg_to_rad(place_rot_deg)
|
|
get_viewport().set_input_as_handled()
|
|
elif selected_index >= 0:
|
|
rotate_selected(ROT_STEP_DEG)
|
|
get_viewport().set_input_as_handled()
|
|
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
|
if placing:
|
|
place_rot_deg = fmod(place_rot_deg - ROT_STEP_DEG + 3600.0, 360.0)
|
|
_ghost.rotation.y = deg_to_rad(place_rot_deg)
|
|
get_viewport().set_input_as_handled()
|
|
elif selected_index >= 0:
|
|
rotate_selected(-ROT_STEP_DEG)
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _on_left_press(mb: InputEventMouseButton) -> void:
|
|
if placing:
|
|
_place_object(mb.position)
|
|
return
|
|
_press_pos = mb.position
|
|
var hit := _pick_object(mb.position)
|
|
if hit >= 0:
|
|
if selected_index != hit:
|
|
select_object(hit)
|
|
_dragging = true
|
|
_drag_moved = false
|
|
var p: Variant = _ground_point(mb.position)
|
|
var marker: Node3D = view_builder.object_markers[selected_index]
|
|
if p != null and marker != null and is_instance_valid(marker):
|
|
_grab_offset = marker.position - (p as Vector3)
|
|
_grab_offset.y = 0.0
|
|
else:
|
|
_grab_offset = Vector3.ZERO
|
|
else:
|
|
# Leere Stelle: Auswahl aufheben (klicken statt auswendig lernen).
|
|
if selected_index != -1:
|
|
clear_selection()
|
|
|
|
|
|
func _on_left_release(_mb: InputEventMouseButton) -> void:
|
|
if _dragging:
|
|
_dragging = false
|
|
if _drag_moved and selected_index >= 0:
|
|
var objects: Array = main.doc.get("objects", [])
|
|
var marker: Node3D = null
|
|
if selected_index < view_builder.object_markers.size():
|
|
marker = view_builder.object_markers[selected_index]
|
|
if selected_index < objects.size() and marker != null and is_instance_valid(marker):
|
|
var before: Dictionary = main.history.snapshot(["objects"])
|
|
var pos := marker.position
|
|
objects[selected_index]["pos"] = [float(pos.x), float(pos.y), float(pos.z)]
|
|
main.history.commit("Objekt verschoben",
|
|
before, main.history.snapshot(["objects"]), "move:%d" % selected_index)
|
|
main.call("_refresh_history_buttons")
|
|
_drag_moved = false
|
|
|
|
|
|
# ---------------------------------------------------- Screenshot-Hilfe ------
|
|
|
|
## Demo-Hook (Desktop-Smoke): Ghost außerhalb echter Maus-Ereignisse parken,
|
|
## damit ein Screenshot die Vorschau zeigen kann.
|
|
func demo_park_ghost(world_pos: Vector3, oid: String) -> void:
|
|
start_placement(oid)
|
|
placing = true
|
|
place_oid = oid
|
|
_ghost_refresh_mesh()
|
|
_ghost.position = world_pos
|
|
_ghost.rotation.y = deg_to_rad(place_rot_deg)
|
|
_ghost.visible = true
|
|
_ghost_active = true
|
|
main.call("_highlight_library", oid)
|
|
main.call("_set_status", "%s: führe die Maus über die Karte — Klick setzt es. Mausrad dreht vorher, Rechtsklick/ESC bricht ab." % main.object_label(oid)) |