143 lines
5.2 KiB
GDScript
143 lines
5.2 KiB
GDScript
extends RefCounted
|
||
class_name SelectionMath
|
||
## Rubberband-Auswahl-Mathematik (Karte t_f6f29d86) — pure, statisch,
|
||
## headless testbar (kein Viewport nötig — Transform + Projection genügen).
|
||
##
|
||
## Ein Objekt gilt als "im Rahmen", wenn sein Anker-Punkt (Fußpunkt der
|
||
## Objekt-Marker) im Bildschirm-Rechteck liegt. Anker statt Mesh-Exakt:
|
||
## schnell und fürs Auge richtig — Marker sind die Klick-Flächen.
|
||
|
||
|
||
## Kern (getestet): Weltpunkte → Bildschirmprojektion mit MITGEBENGER
|
||
## Kamera-Transform + Projektionsmatrix. Punkte hinter der Kamera
|
||
## (Blickrichtung -Z, view.z >= 0) werden ignoriert.
|
||
static func pick_in_rect_projection(cam_xform: Transform3D, proj: Projection,
|
||
viewport_size: Vector2, world_positions: Array, rect: Rect2) -> Array:
|
||
var out: Array = []
|
||
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
|
||
return out
|
||
var inv := cam_xform.affine_inverse()
|
||
for i in world_positions.size():
|
||
var p: Variant = world_positions[i]
|
||
if not (p is Vector3):
|
||
continue
|
||
var view: Vector3 = inv * (p as Vector3)
|
||
if view.z >= 0.0:
|
||
continue # hinter der Kamera (kein Spiegel-Treffer)
|
||
var clip: Vector4 = proj * Vector4(view.x, view.y, view.z, 1.0)
|
||
if absf(clip.w) < 0.0000001:
|
||
continue
|
||
var ndc := Vector2(clip.x / clip.w, clip.y / clip.w)
|
||
var screen := Vector2((ndc.x * 0.5 + 0.5) * viewport_size.x,
|
||
(1.0 - (ndc.y * 0.5 + 0.5)) * viewport_size.y)
|
||
if rect.has_point(screen):
|
||
out.append(i)
|
||
return out
|
||
|
||
|
||
## Runtime-Variante (Editor): nimmt die Viewport-Kamera direkt.
|
||
static func pick_in_rect(cam: Camera3D, world_positions: Array, rect: Rect2) -> Array:
|
||
if cam == null or not is_instance_valid(cam):
|
||
return []
|
||
var viewport_size := Vector2(cam.get_viewport().size)
|
||
return pick_in_rect_projection(cam.global_transform, cam.get_camera_projection(),
|
||
viewport_size, world_positions, rect)
|
||
|
||
|
||
## Normalisiertes Rechteck aus zwei Ecken (auch "rückwärts" aufgezogen).
|
||
static func rect_from(a: Vector2, b: Vector2) -> Rect2:
|
||
return Rect2(Vector2(minf(a.x, b.x), minf(a.y, b.y)),
|
||
Vector2(absf(b.x - a.x), absf(b.y - a.y)))
|
||
|
||
|
||
## Mittelpunkt der Auswahl für "F = Auswahl ins Bild zentrieren"
|
||
## (Karte t_c46d31c2): Mittelwert der Marker-Anker (+ halbe Mesh-Höhe — der
|
||
## optische Objekt-Mittelpunkt, nicht der Fußpunkt). Ungültige Indizes/Marker
|
||
## werden übersprungen; bei GAR NICHTS Gültigem: Ursprung (main meldet dann
|
||
## „nichts ausgewählt“ — der Guard sitzt beim Aufrufer).
|
||
static func focus_center(markers: Array, indices: Array) -> Vector3:
|
||
var sum := Vector3.ZERO
|
||
var count := 0
|
||
for i in indices:
|
||
if not (i is int) or i < 0 or i >= markers.size():
|
||
continue
|
||
var m: Variant = markers[i]
|
||
if m == null or not (m is Node3D) or not is_instance_valid(m):
|
||
continue
|
||
var node := m as Node3D
|
||
sum += node.global_position \
|
||
+ Vector3.UP * float(node.get_meta("half_height", 1.0)) * 0.5
|
||
count += 1
|
||
if count == 0:
|
||
return Vector3.ZERO
|
||
return sum / float(count)
|
||
|
||
|
||
# ------------------------------------------------- Exakter Klick-Treffer ---
|
||
|
||
## Faces-Cache pro Mesh (Klick-Pick, Karte Mauer-Klick 24.10.): get_faces()
|
||
## kopiert jedes Mal das ganze Dreiecks-Array — einmal pro Mesh reicht.
|
||
static var _faces_cache: Dictionary = {}
|
||
|
||
|
||
## Strahl gegen die ECHTEN Dreiecke eines Meshes (Möller–Trumbore über
|
||
## ray_tri). xform = globale Transform der MeshInstance (inkl.
|
||
## Objekt-Drehung/-Kippung, Platzhalter-Lift und Node-Scale).
|
||
## Rückgabe: Welt-Abstand vom Strahl-Ursprung zum nächsten Treffer, -1 = keiner.
|
||
## WARUM: Die Kugel-Näherung (pick_radius = halbe AABB-Diagonale) eines großen
|
||
## Gebäudes ragt weit über seine Wände hinaus — ein Klick auf die schmale
|
||
## Mauer daneben traf zuerst die Gebäude-Kugel (Sly-Befund 22.10.).
|
||
static func ray_mesh_distance(xform: Transform3D, faces: PackedVector3Array,
|
||
origin: Vector3, dir: Vector3) -> float:
|
||
if faces.size() < 3:
|
||
return -1.0
|
||
var inv := xform.affine_inverse()
|
||
var lo: Vector3 = inv * origin
|
||
var ld: Vector3 = inv.basis * dir
|
||
var best := INF
|
||
for i in int(faces.size() / 3):
|
||
var t := ray_tri(lo, ld, faces[i * 3], faces[i * 3 + 1], faces[i * 3 + 2])
|
||
if t >= 0.0 and t < best:
|
||
best = t
|
||
if best == INF:
|
||
return -1.0
|
||
return origin.distance_to(xform * (lo + ld * best))
|
||
|
||
|
||
## Faces eines Meshes (gecacht). Leeres Array, wenn kein Mesh.
|
||
static func mesh_faces(mesh: Mesh) -> PackedVector3Array:
|
||
if mesh == null:
|
||
return PackedVector3Array()
|
||
var key := mesh.get_rid()
|
||
if not _faces_cache.has(key):
|
||
_faces_cache[key] = mesh.get_faces()
|
||
return _faces_cache[key]
|
||
|
||
|
||
## Objekt-Mesh eines Markers: das Modell ist laut
|
||
## map_view_builder._build_objects IMMER das erste MeshInstance3D-Kind.
|
||
static func marker_mesh(marker: Node3D) -> MeshInstance3D:
|
||
for child in marker.get_children():
|
||
if child is MeshInstance3D:
|
||
return child as MeshInstance3D
|
||
return null
|
||
|
||
|
||
## Möller–Trumbore: Abstand t entlang des Strahls zum Dreieck (a, b, c), -1 = kein Treffer.
|
||
static func ray_tri(origin: Vector3, dir: Vector3, a: Vector3, b: Vector3, c: Vector3) -> float:
|
||
var e1 := b - a
|
||
var e2 := c - a
|
||
var p := dir.cross(e2)
|
||
var det := e1.dot(p)
|
||
if absf(det) < 0.000001:
|
||
return -1.0
|
||
var inv := 1.0 / det
|
||
var tv := origin - a
|
||
var u := tv.dot(p) * inv
|
||
if u < 0.0 or u > 1.0:
|
||
return -1.0
|
||
var q := tv.cross(e1)
|
||
var v := dir.dot(q) * inv
|
||
if v < 0.0 or u + v > 1.0:
|
||
return -1.0
|
||
return e2.dot(q) * inv
|