Terrain C (t_4092e3a0): Objekt-Platzierung == Ghost-Preview — EIN Platzier-Pfad, Fusspunkt auf Terrainhoehe.
Root-Cause: Ghost-Preview und Platzierung nutzten zwei unterschiedliche Pfade/Strukturen — der Ghost-Root trug den halben Mesh-Lift selbst (Root=Y+half), das platzierte Objekt hebt nur das Kind-Mesh; die Platzierung rastete X/Z und hob Y auf terrain_height_at des UNGERASTETEN Punkts. Bei Raster-Snap und an Haengen wich die platzierte Position von der Vorschau ab. Fix — EIN Pfad (ObjectOps.placement_foot): Raster-Snap auf X/Z, dann Y = Terrainhoehe am GERASTETEN Punkt (SvmapDocument.terrain_height_at). Ghost-Root = Fusspunkt, Kind-Mesh um half_height gehoben — identische Struktur wie map_view_builder. Objekt-Cursor trifft jetzt das GELAENDE (TerrainField.raycast, gleicher Pfad wie Terrain-/Bauen-/Spawn-Werkzeug) statt der flachen Basis-Ebene; Hoehen-Cache dekodiert einmal pro Struktur-Aenderung (refresh_terrain_cache aus _after_structure_change). Tests (test_library 40->94 Checks): Platzier-Fusspunkt (Rampe 0..20 m: Fusspunkt == Terrainhoehe, Ghost-Pfad == Platzierungs-Pfad, Raster-Snap-Y am gerasteten Punkt), Registry-Fusspunkt (alle 54 Objekte: Mesh-Unterseite == pos, kein Schweben/Eingraben), Ghost-Preview == platziertes Objekt mit simulierter Kamera (Treffer-Zelle == Cursor-Zelle, <5 cm). Editor-Suiten 501 Checks gruen, Builder-Export 21/0, GUI-Smoke 20 gruen.
This commit is contained in:
parent
e505329a42
commit
b20dff2b5d
4 changed files with 274 additions and 16 deletions
|
|
@ -31,9 +31,21 @@ var view_builder # script-erweitert, Variant
|
|||
var placing := false
|
||||
var place_oid := ""
|
||||
var place_rot_deg := 0.0
|
||||
var _ghost: MeshInstance3D
|
||||
var _ghost: Node3D # Ghost-ROOT: Position = Objekt-Fußpunkt —
|
||||
# identisch zur platzierten Marker-Position
|
||||
# (Karte t_4092e3a0: Ghost == Platzierung).
|
||||
var _ghost_mi: MeshInstance3D # Kind-Mesh, gehoben um half_height —
|
||||
# exakt wie der Viewport-Bauer (map_view_builder).
|
||||
var _ghost_active := false # Ghost sichtbar weil Cursor über der Map ist
|
||||
|
||||
# Terrain-Höhen-Cache (Karte t_4092e3a0): der Objekt-Cursor trifft das GELÄNDE
|
||||
# (TerrainField.raycast, wie Terrain-/Bauen-/Spawn-Werkzeug), nicht mehr nur
|
||||
# die flache Basis-Ebene. Dekodiert einmal pro Map-Änderung, nicht pro Mausbewegung.
|
||||
var _heights := PackedFloat32Array()
|
||||
var _res := 0
|
||||
var _sx := 0.0
|
||||
var _sz := 0.0
|
||||
|
||||
# Auswahl + Greifen
|
||||
var selected_index := -1 # PRIMÄR-Objekt (letzter Klick) — Grab/Status-Anker
|
||||
var selected_indices: Array = [] # komplette Auswahl in Klick-Reihenfolge (int-Indizes)
|
||||
|
|
@ -61,6 +73,7 @@ func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
|||
main = p_main
|
||||
camera = p_camera
|
||||
view_builder = p_builder
|
||||
refresh_terrain_cache() # t_4092e3a0: Cursor trifft das Gelände ab der ersten Mausbewegung
|
||||
|
||||
|
||||
## Kamera fragt: dreht das Mausrad gerade ein Objekt/eine Vorschau
|
||||
|
|
@ -69,14 +82,35 @@ func wheel_blocked() -> bool:
|
|||
return placing or selected_index >= 0
|
||||
|
||||
|
||||
## Höhen-Cache auffrischen (Karte t_4092e3a0): NACH Terrain-Edits, Undo/Redo,
|
||||
## Laden/Neu — main ruft das in _after_structure_change. Ein Decode pro
|
||||
## Änderung statt pro Mausbewegung (129×129-Quantisierung bleibt billig).
|
||||
func refresh_terrain_cache() -> void:
|
||||
var terrain: Dictionary = main.doc.get("terrain", {})
|
||||
_res = int(terrain.get("resolution", 0))
|
||||
_sx = float(terrain.get("size_x_m", 0.0))
|
||||
_sz = float(terrain.get("size_z_m", 0.0))
|
||||
if _res >= 2 and _sx > 0.0 and _sz > 0.0:
|
||||
_heights = SvmapIO.decode_heights(terrain)
|
||||
else:
|
||||
_heights = PackedFloat32Array()
|
||||
|
||||
|
||||
func _ground_point(viewport_pos: Vector2) -> Variant:
|
||||
# Cursor-Strahl → Punkt auf der Boden-Referenzebene (Terrain-Basis).
|
||||
# Cursor-Strahl → erster Treffer AUF DEM GELÄNDE (Karte t_4092e3a0): die
|
||||
# Höhenform zählt, nicht die flache Basis-Ebene — sonst landet ein Objekt
|
||||
# auf einem Hügel versetzt hinter dem Cursor. Gleicher Pfad wie Terrain-,
|
||||
# Bauen- und Spawn-Werkzeug (TerrainField.raycast). Rückgabe null = kein
|
||||
# Treffer (Strahl ins Leere / außerhalb der Map).
|
||||
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 _res >= 2 and _heights.size() >= _res * _res:
|
||||
return TerrainField.raycast(_heights, _res, _sx, _sz, origin, dir)
|
||||
# Fallback (Map ohne brauchbares Höhenfeld): Basis-Ebene wie bisher.
|
||||
var plane_y := float(terrain.get("min_height_m", 0.0))
|
||||
if absf(dir.y) < 0.0001:
|
||||
return null
|
||||
var t := (plane_y - origin.y) / dir.y
|
||||
|
|
@ -127,6 +161,7 @@ func start_placement(oid: String) -> void:
|
|||
placing = true
|
||||
place_rot_deg = 0.0
|
||||
_ghost_active = false
|
||||
refresh_terrain_cache() # Cursor trifft das GELÄNDE — Cache muss frisch sein
|
||||
_ensure_ghost()
|
||||
_ensure_ghost_visible()
|
||||
var label: String = main.object_label(oid)
|
||||
|
|
@ -147,13 +182,18 @@ func cancel_placement() -> void:
|
|||
func _ensure_ghost() -> void:
|
||||
if _ghost != null and is_instance_valid(_ghost):
|
||||
return
|
||||
_ghost = MeshInstance3D.new()
|
||||
# Struktur WIE der Viewport-Bauer (map_view_builder._build_objects):
|
||||
# Root auf dem Fußpunkt, Kind-Mesh um half_height gehoben. So kann Ghost
|
||||
# und platziertes Objekt nie auseinanderfallen (Karte t_4092e3a0).
|
||||
_ghost = Node3D.new()
|
||||
_ghost_mi = 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
|
||||
_ghost_mi.material_override = mat
|
||||
_ghost.add_child(_ghost_mi)
|
||||
add_child(_ghost)
|
||||
_ghost.visible = false
|
||||
|
||||
|
|
@ -163,8 +203,9 @@ func _ghost_refresh_mesh() -> void:
|
|||
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)
|
||||
_ghost_mi.mesh = mesh
|
||||
# Gleicher Lift wie beim platzierten Objekt: Mesh-Unterseite = Fußpunkt.
|
||||
_ghost_mi.position.y = MeshShapes.half_height(mesh)
|
||||
|
||||
|
||||
func _ensure_ghost_visible() -> void:
|
||||
|
|
@ -190,12 +231,10 @@ func _ghost_update(viewport_pos: Vector2) -> void:
|
|||
_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
|
||||
# EIN Pfad für Ghost UND Platzierung (Karte t_4092e3a0): Fußpunkt =
|
||||
# Terrain-Höhe am gerasteten Punkt. Der sichtbare Lift hängt am Kind-Mesh.
|
||||
_ghost.position = ObjectOps.placement_foot(
|
||||
main.doc, p as Vector3, bool(main.get("grid_snap")), _terrain_half(), true)
|
||||
_ghost.visible = true
|
||||
|
||||
|
||||
|
|
@ -204,9 +243,10 @@ func _place_object(viewport_pos: Vector2) -> void:
|
|||
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:
|
||||
var foot: Vector3 = ObjectOps.placement_foot(
|
||||
main.doc, p as Vector3, bool(main.get("grid_snap")), half, false)
|
||||
if absf(foot.x) > half.x or absf(foot.z) > half.y:
|
||||
main.call("_set_status", "Das wäre außerhalb der Map gelandet — klicke innerhalb der grünen Fläche.", true)
|
||||
return
|
||||
|
||||
|
|
@ -223,7 +263,9 @@ func _place_object(viewport_pos: Vector2) -> void:
|
|||
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)]
|
||||
# EIN Pfad für Ghost UND Platzierung (Karte t_4092e3a0): derselbe Fußpunkt
|
||||
# wie die Vorschau — Raster-Snap + Terrain-Höhe am gerasteten Punkt.
|
||||
var pos_f := [foot.x, foot.y, foot.z]
|
||||
(main.doc["objects"] as Array).append({
|
||||
"id": place_oid,
|
||||
"pos": pos_f,
|
||||
|
|
|
|||
|
|
@ -129,3 +129,28 @@ static func lift_offset(doc: Dictionary, index: int) -> float:
|
|||
if (pos as Array).size() != 3:
|
||||
return 0.0
|
||||
return float(pos[1]) - SvmapDocument.terrain_height_at(doc, float(pos[0]), float(pos[2]))
|
||||
|
||||
|
||||
# ------------------------------------------------- Platzier-Fußpunkt (C) ----
|
||||
# Karte t_4092e3a0 (Gerätebefund: „Ghost saß an der Unterlage, platziert saß
|
||||
# das Objekt HÖHER"): Ghost-Preview und Platzierung MÜSSEN denselben Pfad
|
||||
# nutzen. Diese eine Funktion ist der Pfad:
|
||||
# 1. Raster-Snap auf X/Z (wenn an),
|
||||
# 2. Y = Terrain-Höhe am GERASTETEN Punkt (SvmapDocument.terrain_height_at)
|
||||
# — das ist der Objekt-Fußpunkt (Konvention, README §Konventionen):
|
||||
# „pos.y = Terrainhöhe an (x, z)“. Der sichtbare Lift (halbe Mesh-Höhe,
|
||||
# aus der Registry-Kollision via MeshShapes.half_height) ist Aufgabe der
|
||||
# Darstellung (Viewport-Bauer UND Ghost-Kind-Mesh machen es identisch).
|
||||
# clamp_to_map: true = Ghost clampft still auf die Map (Vorschau bleibt
|
||||
# sichtbar am Rand); false = Platzierung bekommt den ungeclampten Punkt und
|
||||
# die UI meldet „außerhalb der Map“.
|
||||
static func placement_foot(doc: Dictionary, hit: Vector3, grid_enabled: bool,
|
||||
half: Vector2, clamp_to_map: bool) -> Vector3:
|
||||
var pos := hit
|
||||
pos.x = snap_coord(pos.x, grid_enabled)
|
||||
pos.z = snap_coord(pos.z, grid_enabled)
|
||||
if clamp_to_map:
|
||||
pos.x = clampf(pos.x, -half.x, half.x)
|
||||
pos.z = clampf(pos.z, -half.y, half.y)
|
||||
pos.y = SvmapDocument.terrain_height_at(doc, pos.x, pos.z)
|
||||
return pos
|
||||
|
|
|
|||
|
|
@ -1687,6 +1687,8 @@ func _after_structure_change() -> void:
|
|||
_sync_grid_to_builder()
|
||||
_refresh_tree()
|
||||
_recheck_terrain_mode()
|
||||
if interaction != null:
|
||||
interaction.call("refresh_terrain_cache") # t_4092e3a0: Objekt-Cursor trifft das Gelände
|
||||
if tool_mode == "spawns" and spawn_tool != null:
|
||||
spawn_tool.call("refresh_from_doc")
|
||||
if tool_mode == "bauen" and build_tool != null:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ var checks := 0
|
|||
|
||||
func _init() -> void:
|
||||
_run_tests()
|
||||
_finish()
|
||||
|
||||
|
||||
## Die Ghost-Gruppe braucht einen Viewport im Baum (Kamera-Projektion) —
|
||||
## erst zwei Frames warten (wie test_terrain/_gui_smoke), dann fahren.
|
||||
func _finish() -> void:
|
||||
await process_frame
|
||||
await process_frame
|
||||
_test_ghost_matches_placement()
|
||||
if failures == 0:
|
||||
print("ALL OK: %d checks" % checks)
|
||||
quit(0)
|
||||
|
|
@ -38,6 +47,8 @@ func _run_tests() -> void:
|
|||
_test_snap_to_ground_undo()
|
||||
_test_crud_undo_all_objects()
|
||||
_test_svmap_roundtrip_objects()
|
||||
_test_placement_foot_convention()
|
||||
_test_mesh_foot_point_registry()
|
||||
|
||||
|
||||
# ------------------------------------------------------- Bibliothek laden ----
|
||||
|
|
@ -323,3 +334,181 @@ func _test_svmap_roundtrip_objects() -> void:
|
|||
"Poly-Budget aus Katalog: %.0f (erwartet %.0f)" % [SvmapDocument.poly_usage(doc, manifest), expected])
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_library_roundtrip.svmap"))
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path("user://test_library_roundtrip2.svmap"))
|
||||
|
||||
|
||||
# --------------------------------------------- Platzier-Fußpunkt (t_4092e3a0) —
|
||||
|
||||
## Probe-Terrain mit bekannter Höhenform: Rampe entlang +X (an der −X-Kante 0,
|
||||
## an der +X-Kante 20 m). Linear → bilinear interpolierte terrain_height_at
|
||||
## liefert die exakte Höhe (R16-Quantisierung ~0,2 mm bei 20 m Spannweite).
|
||||
func _ramp_doc() -> Dictionary:
|
||||
var doc := SvmapDocument.create_new("Fusspunkt-Probe", 320.0, "mittag")
|
||||
var terrain: Dictionary = doc["terrain"]
|
||||
var res := 33
|
||||
var size := 320.0
|
||||
terrain["resolution"] = res
|
||||
terrain["size_x_m"] = size
|
||||
terrain["size_z_m"] = size
|
||||
terrain["min_height_m"] = 0.0
|
||||
terrain["max_height_m"] = 20.0
|
||||
var heights := PackedFloat32Array()
|
||||
heights.resize(res * res)
|
||||
var cell := size / float(res - 1)
|
||||
for zi in res:
|
||||
for xi in res:
|
||||
var x := -size * 0.5 + float(xi) * cell
|
||||
heights[zi * res + xi] = 20.0 * (x / size + 0.5)
|
||||
terrain["heightmap_r16_b64"] = TerrainField.encode(heights, 0.0, 20.0)
|
||||
return doc
|
||||
|
||||
|
||||
## Der EINE Platzier-Pfad (ObjectOps.placement_foot): Fußpunkt == Terrainhöhe
|
||||
## am (gerasteten) Punkt; Ghost-Pfad (geclampt) und Platzierungs-Pfad fallen
|
||||
## innerhalb der Map auf DENSSELBEN Wert. Erhöhtes Terrain: Y folgt der Form.
|
||||
func _test_placement_foot_convention() -> void:
|
||||
print("Testgruppe: Platzier-Fußpunkt (Ghost == Platzierung == Terrainhöhe)")
|
||||
var doc := _ramp_doc()
|
||||
var half := Vector2(160.0, 160.0)
|
||||
var probes := [Vector3(-120.0, 7.0, 0.0), Vector3(0.0, 7.0, 0.0),
|
||||
Vector3(80.0, 7.0, 40.0), Vector3(-80.0, 7.0, -120.0)]
|
||||
for p in probes:
|
||||
var h := SvmapDocument.terrain_height_at(doc, p.x, p.z)
|
||||
var foot: Vector3 = ObjectOps.placement_foot(doc, p, false, half, false)
|
||||
_check(absf(foot.y - h) < 0.001,
|
||||
"Fußpunkt == Terrainhöhe an (%.0f, %.0f): %.3f m" % [p.x, p.z, foot.y])
|
||||
var ghost: Vector3 = ObjectOps.placement_foot(doc, p, false, half, true)
|
||||
_check(ghost.distance_to(foot) < 0.0001,
|
||||
"Ghost-Pfad == Platzierungs-Pfad (in der Map) bei (%.0f, %.0f)" % [p.x, p.z])
|
||||
# Erhöhtes Terrain (Karte A): Platzierung folgt der Form, nicht Y=0.
|
||||
var mid := ObjectOps.placement_foot(doc, Vector3(0.0, 0.0, 0.0), false, half, false)
|
||||
_check(absf(mid.y - 10.0) < 0.01, "Rampe in der Mitte: Fußpunkt ≈ 10 m (ist %.3f)" % mid.y)
|
||||
# Raster-Snap: X/Z ganze Meter, Y = Terrainhöhe am GERASTETEN Punkt.
|
||||
var snap := ObjectOps.placement_foot(doc, Vector3(10.6, 0.0, 0.6), true, half, true)
|
||||
_check(snap.x == 11.0 and snap.z == 1.0, "Raster-Snap rastet X/Z (10.6/0.6 → 11/1)")
|
||||
_check(absf(snap.y - SvmapDocument.terrain_height_at(doc, 11.0, 1.0)) < 0.001,
|
||||
"Y = Terrainhöhe am gerasteten Punkt")
|
||||
# Außerhalb der Map: Platzierung bekommt den ungeclampten Punkt (die UI
|
||||
# meldet „außerhalb“), der Ghost clampft still auf die Grenze.
|
||||
var outp := ObjectOps.placement_foot(doc, Vector3(500.0, 0.0, 0.0), false, half, false)
|
||||
_check(absf(outp.x - 500.0) < 0.001, "Platzierung: ungeclampt außerhalb erkennbar")
|
||||
var clamped := ObjectOps.placement_foot(doc, Vector3(500.0, 0.0, 0.0), false, half, true)
|
||||
_check(absf(clamped.x - 160.0) < 0.001, "Ghost: clampft still auf die Map-Grenze")
|
||||
|
||||
|
||||
## Regression über ALLE Registry-Objekte (Fußpunkt-Konvention): die
|
||||
## Platzhalter-Meshes sind so gebaut, dass ihre UNTERSEITE auf dem Objekt-
|
||||
## Fußpunkt (pos.y) sitzt — kein Schweben, kein Eingraben. Der Lift, den
|
||||
## Ghost UND Viewport-Bauer nutzen, kommt aus derselben Registry-Kollision
|
||||
## (MeshShapes.half_height).
|
||||
func _test_mesh_foot_point_registry() -> void:
|
||||
print("Testgruppe: Registry-Fußpunkt — Mesh-Unterseite == pos (alle 54 Objekte)")
|
||||
var manifest := _load_manifest()
|
||||
var lib: Dictionary = manifest.get("objects", {})
|
||||
var bad_primitiv: Array = []
|
||||
var bad_verbund: Array = []
|
||||
for oid in lib.keys():
|
||||
var entry: Dictionary = lib[oid]
|
||||
var col: Dictionary = entry.get("collision", {})
|
||||
var shape := str(col.get("shape", "box"))
|
||||
var mesh := MeshShapes.make_placeholder(shape, col, 1.0)
|
||||
if mesh == null:
|
||||
bad_verbund.append(str(oid) + " (mesh null)")
|
||||
continue
|
||||
var hh := MeshShapes.half_height(mesh)
|
||||
var bottom := mesh.get_aabb().position.y + hh
|
||||
if shape == "compound":
|
||||
# Struktur-Objekte sind in Fußpunkt-Koordinaten GEBACKEN (hh = 0):
|
||||
# nichts schwebt ÜBER dem Fußpunkt; Rampenenden dürfen den Boden
|
||||
# knapp berühren (bruecke_holz ≈ −0,31 m an der Rampenkante).
|
||||
if absf(hh) > 0.0001 or bottom > 0.05 or bottom < -0.4:
|
||||
bad_verbund.append("%s (bottom %.3f)" % [oid, bottom])
|
||||
else:
|
||||
if absf(bottom) > 0.05:
|
||||
bad_primitiv.append("%s (bottom %.3f)" % [oid, bottom])
|
||||
_check(bad_primitiv.is_empty(),
|
||||
"alle Grundformen: Mesh-Unterseite == Fußpunkt (±5 cm) — %s" % str(bad_primitiv))
|
||||
_check(bad_verbund.is_empty(),
|
||||
"alle Verbund-Objekte: fußpunkt-gebacken (kein Schweben) — %s" % str(bad_verbund))
|
||||
var crate := MeshShapes.make_placeholder("box", {"size": [1.2, 1.2, 1.2]}, 1.0)
|
||||
_check(absf(MeshShapes.half_height(crate) - 0.6) < 0.0001,
|
||||
"half_height(Box 1.2 m) = 0.6 — Registry-Kollision ist der Lift")
|
||||
|
||||
|
||||
## Minimales main-Doppel für die Ghost-Gruppe (interaction braucht nur doc,
|
||||
## manifest und grid_snap — keine UI).
|
||||
class ProbeMain:
|
||||
var doc: Dictionary = {}
|
||||
var manifest: Dictionary = {}
|
||||
var grid_snap := false
|
||||
|
||||
|
||||
## Akzeptanz-Test (Karte t_4092e3a0): die ECHTE Ghost-Preview-Logik
|
||||
## (interaction._ghost_update mit simulierter Kamera über der Rampe) muss
|
||||
## denselben Fußpunkt liefern wie die Platzierung — Objekt-Unterseite auf
|
||||
## Terrainhöhe (< 5 cm), Ghost-Y == platzierte Y.
|
||||
func _test_ghost_matches_placement() -> void:
|
||||
print("Testgruppe: Ghost-Preview == platziertes Objekt (simulierte Kamera)")
|
||||
var doc := _ramp_doc()
|
||||
var fake := ProbeMain.new()
|
||||
fake.doc = doc
|
||||
fake.manifest = _load_manifest()
|
||||
var size := 320.0
|
||||
var half := Vector2(160.0, 160.0)
|
||||
var cell := size / 32.0
|
||||
var vp := SubViewport.new()
|
||||
vp.size = Vector2i(1280, 720)
|
||||
root.add_child(vp)
|
||||
var cam := Camera3D.new()
|
||||
vp.add_child(cam)
|
||||
cam.current = true
|
||||
cam.look_at_from_position(Vector3(-95.0, 115.0, 135.0), Vector3.ZERO, Vector3.UP)
|
||||
var interaction := Node3D.new()
|
||||
interaction.set_script(load("res://src/editor/interaction.gd"))
|
||||
vp.add_child(interaction)
|
||||
interaction.setup(fake, cam, null)
|
||||
interaction.set("place_oid", "crate_wood")
|
||||
interaction.set("placing", true)
|
||||
interaction.call("_ensure_ghost")
|
||||
interaction.call("_ghost_refresh_mesh")
|
||||
var targets := [Vector2(-120.0, 0.0), Vector2(0.0, 0.0),
|
||||
Vector2(80.0, 40.0), Vector2(-40.0, -80.0)]
|
||||
for tp in targets:
|
||||
var h := SvmapDocument.terrain_height_at(doc, tp.x, tp.y)
|
||||
var screen := cam.unproject_position(Vector3(tp.x, h, tp.y))
|
||||
interaction.call("_ghost_update", screen)
|
||||
var ghost: Node3D = interaction.get("_ghost")
|
||||
var visible := ghost != null and is_instance_valid(ghost) and ghost.visible
|
||||
_check(visible, "Ghost sichtbar bei Ziel %s (Cursor trifft Gelände)" % tp)
|
||||
if not visible:
|
||||
continue
|
||||
_check(absf(ghost.position.x - tp.x) < cell and absf(ghost.position.z - tp.y) < cell,
|
||||
"Ghost-Treffer == Cursor-Zelle bei %s (Abweichung %.2f/%.2f m, Zelle %.1f m)" % [
|
||||
tp, absf(ghost.position.x - tp.x), absf(ghost.position.z - tp.y), cell])
|
||||
var th := SvmapDocument.terrain_height_at(doc, ghost.position.x, ghost.position.z)
|
||||
_check(absf(ghost.position.y - th) < 0.05,
|
||||
"Ghost-Fußpunkt == Terrainhöhe (<5 cm) bei %s (Δ %.4f m)" % [tp, absf(ghost.position.y - th)])
|
||||
var place_foot: Vector3 = ObjectOps.placement_foot(doc, ghost.position, false, half, false)
|
||||
_check(place_foot.distance_to(ghost.position) < 0.0001,
|
||||
"Platzierung an derselben Stelle == Ghost-Position (Δ %.5f m)" % place_foot.distance_to(ghost.position))
|
||||
var mi: MeshInstance3D = ghost.get_child(0)
|
||||
var mesh: Mesh = mi.mesh
|
||||
# Welt-Unterseite = Fußpunkt (Root) + Lift (Kind) + AABB-Min (Mesh).
|
||||
var bottom := ghost.position.y + mi.position.y + mesh.get_aabb().position.y
|
||||
_check(absf(bottom - ghost.position.y) < 0.05,
|
||||
"Ghost-Mesh-Unterseite sitzt auf dem Fußpunkt (Δ %.4f m)" % absf(bottom - ghost.position.y))
|
||||
# Raster-Snap am Ghost: gerastete X/Z, Y folgt der GERASTETEN Position.
|
||||
fake.grid_snap = true
|
||||
var h_snap := SvmapDocument.terrain_height_at(doc, 10.0, 0.0)
|
||||
var screen_snap := cam.unproject_position(Vector3(10.0, h_snap, 0.0))
|
||||
interaction.call("_ghost_update", screen_snap)
|
||||
var g2: Node3D = interaction.get("_ghost")
|
||||
if g2 != null and g2.visible:
|
||||
_check(absf(g2.position.x - roundf(g2.position.x)) < 0.0001
|
||||
and absf(g2.position.z - roundf(g2.position.z)) < 0.0001,
|
||||
"Ghost mit Raster-Snap: X/Z ganze Meter (%.2f/%.2f)" % [g2.position.x, g2.position.z])
|
||||
var th2 := SvmapDocument.terrain_height_at(doc, g2.position.x, g2.position.z)
|
||||
_check(absf(g2.position.y - th2) < 0.05,
|
||||
"Ghost mit Raster-Snap: Y = Terrainhöhe am gerasteten Punkt (Δ %.4f m)" % absf(g2.position.y - th2))
|
||||
else:
|
||||
_check(false, "Ghost mit Raster-Snap sichtbar")
|
||||
vp.free()
|
||||
|
|
|
|||
Loading…
Reference in a new issue