scopedvr-map-core/editor/selection_math.gd
slyphax 17a8927fec Map-Kern aus dem Editor übernommen (Stand e8ab954)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:30:18 +01:00

143 lines
5.2 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 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