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