extends SceneTree ## Editor-Tests (headless): M2 Terrain spiegeln (Karte t_f9eff526, Spec ## /opt/data/scopedvr/KARTEN_MIRROR_vc27.md) — im Symmetrie-Modus schreibt ## jeder Terrain-Strich (Heben/Senken/Glätten/Malen) seine Zellen EXAKT ## gespiegelt in die andere Kartenseite. ## Ausführen: godot --headless --path . --script res://tests/test_terrain_mirror.gd ## Exit 0 = alles grün. ## ## Geprüft wird (Spec-Abnahme): ## 1. Strich in einer Hälfte → beide Zellhälften identisch (Höhe UND Material) ## für alle drei Modi (Punkt / Achse X / Achse Z) und alle vier Pinsel. ## 2. Strich genau auf der Rastermitte bzw. auf der Spiegelachse dupliziert ## NICHT — das Ergebnis ist byte-identisch zu EINER Pinsel-Anwendung ## (Referenz über TerrainField direkt, Gegenprobe gegen den Primitive). ## 3. Strich am Rand: Ränder bleiben exakt gespiegelt, kein Absturz. ## 4. Modus AUS = Verhalten wie vorher: byte-identisches Ergebnis zur ## direkten TerrainField-Anwendung (Gegenprobe; zusätzlich wird der ## gleiche Probe-Strich gegen einen origin/main-Checkout gefahren — ## siehe /opt/data/scopedvr/work/m2_beweise/). ## 5. Live-Rebuild-Pfad: die per _process an update_terrain gegebenen ## Mesh-Inputs (Höhen UND Material-Schicht) sind bereits gespiegelt. ## 6. Ein Strich = EIN Undo-Schritt, Undo stellt BEIDE Hälften zurück. ## 7. Zellgleichheit auch wenn der Pinsel-Fußabdruck die Mitte ÜBERLAPPT ## (Strich-nahe Seite gewinnt, deterministisch). const RES := 33 const SIZE := 320.0 const HMIN := 0.0 const HMAX := 60.0 const STRENGTH := 8.0 # terrain_tool-Default (m/s Heben/Senken) const STROKE_SEC := 0.5 # Strichlänge der Proben const RADIUS := 30.0 var failures := 0 var checks := 0 func _init() -> void: _run_tests() if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) func _check(cond: bool, label: String) -> void: checks += 1 if cond: print(" ok: %s" % label) else: failures += 1 print(" FAIL: %s" % label) func _run_tests() -> void: _test_half_stroke_all_modes() _test_center_stroke_no_duplicate() _test_axis_stroke_no_duplicate() _test_edge_stroke() _test_overlap_stroke_near_center() _test_off_mode_unchanged() _test_live_rebuild_inputs() _test_undo_one_step() # ------------------------------------------------------------- Hilfsbau ----- class ToolMain: var doc: Dictionary = {} var sym_mode: int = SymmetryCore.Mode.OFF var history: ActionHistory = ActionHistory.new() var commits := 0 func _set_status(_text: String, _err := false) -> void: pass func _on_terrain_stroke_committed() -> void: commits += 1 class ToolBuilder: var last_heights := PackedFloat32Array() var last_mats := PackedByteArray() var calls := 0 var last_splat_rect := Rect2i() # t_7a0a6b79: Live-Splat-Sub-Rect func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray(), splat_rect: Rect2i = Rect2i()) -> void: last_heights = heights last_mats = mats last_splat_rect = splat_rect calls += 1 ## Kleine Test-Map (33² über 320 m, flach, alles Gras) — schnell genug für ## Voll-Raster-Vergleiche, mit Rastermitte (ungerade res → Selbstabbildung). func _make_doc() -> Dictionary: var doc := SvmapDocument.create_new("Mirror", SIZE, "mittag") var terrain: Dictionary = doc["terrain"] terrain["resolution"] = RES terrain["size_x_m"] = SIZE terrain["size_z_m"] = SIZE terrain["min_height_m"] = HMIN terrain["max_height_m"] = HMAX var flat := PackedFloat32Array() flat.resize(RES * RES) terrain["heightmap_r16_b64"] = TerrainField.encode(flat, HMIN, HMAX) var mats := PackedByteArray() mats.resize(RES * RES) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) terrain.erase("material_resolution") # Test-Raster: Material = Höhenraster (Alt-Karte) return doc func _make_tool(sym: int) -> Array: var doc := _make_doc() var tmain := ToolMain.new() tmain.doc = doc tmain.sym_mode = sym tmain.history.begin(doc) var builder := ToolBuilder.new() var ttool := Node3D.new() ttool.set_script(load("res://src/editor/terrain_tool.gd")) ttool.setup(tmain, Camera3D.new(), builder, null) var activated: bool = ttool.call("activate") _check(activated, "terrain_tool aktiviert (Sym-Modus %d)" % sym) ttool.call("set_radius", RADIUS) return [ttool, tmain, builder, doc] ## Programmgesteuerter Strich über den ECHTEN Pfad (demo_stroke: begin → ## Schritte → end → Commit → Refresh — exakt wie ein Maus-Strich). func _stroke(ttool, x: float, z: float, mode: int, seconds: float) -> void: ttool.call("demo_stroke", x, z, mode, seconds) func _decode_heights(doc: Dictionary) -> PackedFloat32Array: return SvmapIO.decode_heights(doc["terrain"]) func _decode_mats(doc: Dictionary) -> PackedByteArray: return SvmapIO.decode_materials(doc["terrain"]) ## Anzahl Zellpaare, deren gespiegelte Schwester einen ANDEREN Wert trägt ## (0 = beide Hälften exakt zellgleich). Höhe. func _count_asym_heights(heights: PackedFloat32Array, mode: int) -> int: var bad := 0 for z: int in RES: for x: int in RES: var mc: Vector2i = SymmetryCore.mirror_cell(mode, x, z, RES) if heights[z * RES + x] != heights[mc.y * RES + mc.x]: bad += 1 return bad ## Anzahl Zellpaare, deren gespiegelte Schwester ein ANDERES Material trägt. func _count_asym_mats(mats: PackedByteArray, mode: int) -> int: var bad := 0 for z: int in RES: for x: int in RES: var mc: Vector2i = SymmetryCore.mirror_cell(mode, x, z, RES) if int(mats[z * RES + x]) != int(mats[mc.y * RES + mc.x]): bad += 1 return bad ## Referenz: N Einzelschritte DIREKT über TerrainField (der Primitive vor M2) — ## für die Gegenproben „Mitte dupliziert nicht" und „AUS wie vorher". func _reference_heights(steps: int, x: float, z: float, mode: int) -> PackedFloat32Array: var ref := PackedFloat32Array() ref.resize(RES * RES) var dt := STROKE_SEC / float(maxi(steps, 1)) var amount := STRENGTH * dt for i in steps: TerrainField.apply_brush(ref, RES, SIZE, SIZE, -100000.0, 100000.0, x, z, RADIUS, mode, amount) return ref # --------------------------------------------------------------- Tests ------ ## 1) Strich in einer Hälfte → beide Zellhälften exakt identisch (Höhe UND ## Material) für alle drei Symmetrie-Modi und alle vier Pinsel-Modi. func _test_half_stroke_all_modes() -> void: print("Testgruppe: Strich in einer Hälfte → Hälften zellgleich (3 Sym-Modi × 4 Pinsel)") var sym_modes := [ ["Punkt", SymmetryCore.Mode.POINT], ["Achse X", SymmetryCore.Mode.AXIS_X], ["Achse Z", SymmetryCore.Mode.AXIS_Z], ] var brush_modes := [ ["Heben", TerrainField.Mode.RAISE], ["Senken", TerrainField.Mode.LOWER], ["Malen", TerrainField.Mode.PAINT], ] for entry0 in sym_modes: var sym_name: String = entry0[0] var sym: int = entry0[1] for entry1 in brush_modes: var brush_name: String = entry1[0] var brush: int = entry1[1] var parts := _make_tool(sym) var ttool = parts[0] var doc: Dictionary = parts[3] if brush == TerrainField.Mode.PAINT: ttool.call("set_paint_material", 1) # Sand statt Default-Gras _stroke(ttool, -100.0, -60.0, brush, STROKE_SEC) var heights := _decode_heights(doc) var mats := _decode_mats(doc) _check(_count_asym_heights(heights, sym) == 0, "%s/%s: Höhen-Hälften exakt zellgleich" % [sym_name, brush_name]) _check(_count_asym_mats(mats, sym) == 0, "%s/%s: Material-Hälften exakt zellgleich" % [sym_name, brush_name]) var moved := false if brush == TerrainField.Mode.PAINT: var pm := _decode_mats(doc) for m in pm: if int(m) != 0: moved = true break else: for v in heights: if v != 0.0: moved = true break _check(moved, "%s/%s: Strich hat tatsächlich gewirkt" % [sym_name, brush_name]) ttool.free() # Malen (Sand) als eigener Material-Strich pro Sym-Modus: var parts_p := _make_tool(sym) var ttool_p = parts_p[0] var doc_p: Dictionary = parts_p[3] ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras _stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3) var mats_p := _decode_mats(doc_p) var sand := 0 for m in mats_p: if int(m) == 1: sand += 1 _check(sand > 0, "%s/Malen: Sand gemalt (%d Zellen)" % [sym_name, sand]) _check(_count_asym_mats(mats_p, sym) == 0, "%s/Malen: Material-Hälften exakt zellgleich (Sand)" % sym_name) var painted_heights := _decode_heights(doc_p) var flat := true for v in painted_heights: if v != 0.0: flat = false break _check(flat, "%s/Malen: Höhe bleibt unangetastet" % sym_name) ttool_p.free() # Glätten braucht Form unter dem Pinsel — symmetrisch vorgeformter Hügel: for entry in sym_modes: var sym_name2: String = entry[0] var sym2: int = entry[1] var parts_s := _make_tool(sym2) var ttool_s = parts_s[0] var tmain_s = parts_s[1] var doc_s: Dictionary = parts_s[3] var terrain: Dictionary = doc_s["terrain"] var hs := _decode_heights(doc_s) var hill := Vector3(-70.0, 0.0, -40.0) var center := SymmetryCore.map_center(doc_s) var twin := SymmetryCore.mirror_point(sym2, hill, center) TerrainField.apply_brush(hs, RES, SIZE, SIZE, -100000.0, 100000.0, hill.x, hill.z, 25.0, TerrainField.Mode.RAISE, 10.0) TerrainField.apply_brush(hs, RES, SIZE, SIZE, -100000.0, 100000.0, twin.x, twin.z, 25.0, TerrainField.Mode.RAISE, 10.0) terrain["heightmap_r16_b64"] = TerrainField.encode(hs, HMIN, HMAX) ttool_s.call("refresh_from_doc") var before := _decode_heights(doc_s) _stroke(ttool_s, -52.0, -28.0, TerrainField.Mode.SMOOTH, STROKE_SEC) var after := _decode_heights(doc_s) var smoothed := false for i in after.size(): if after[i] != before[i]: smoothed = true break _check(smoothed, "%s/Glätten: Strich hat geglättet" % sym_name2) _check(_count_asym_heights(after, sym2) == 0, "%s/Glätten: Höhen-Hälften exakt zellgleich" % sym_name2) ttool_s.free() # Undo-/Commit-Verhalten testet _test_undo_one_step (Strich-Summe oben OK). ## 2) Strich GENAU auf der Rastermitte (Punkt) dupliziert NICHT: Ergebnis ist ## byte-identisch zu EINER Pinsel-Anwendung pro Schritt (Referenz). func _test_center_stroke_no_duplicate() -> void: print("Testgruppe: Strich genau auf der Mitte dupliziert nicht (Punkt)") var parts := _make_tool(SymmetryCore.Mode.POINT) var ttool = parts[0] var doc: Dictionary = parts[3] _stroke(ttool, 0.0, 0.0, TerrainField.Mode.RAISE, STROKE_SEC) var steps := int(ceil(STROKE_SEC / 0.05)) var ref := _reference_heights(steps, 0.0, 0.0, TerrainField.Mode.RAISE) var ref_b64 := TerrainField.encode(ref, HMIN, HMAX) var got_b64 := str(doc["terrain"]["heightmap_r16_b64"]) _check(got_b64 == ref_b64, "Mitte-Strich (Punkt): byte-identisch zu EINER Anwendung (kein Doppel-Heben)") var heights := _decode_heights(doc) _check(_count_asym_heights(heights, SymmetryCore.Mode.POINT) == 0, "Mitte-Strich (Punkt): Hälften exakt zellgleich") var center_h := heights[(RES / 2) * RES + (RES / 2)] _check(absf(center_h - (STRENGTH * STROKE_SEC)) < 0.05, "Mitte-Zelle exakt EINmal gehoben (%.3f m statt %.3f m)" % [ center_h, 2.0 * STRENGTH * STROKE_SEC]) ttool.free() ## 2b) Strich GENAU auf der Spiegelachse dupliziert nicht (Achse X und Z). func _test_axis_stroke_no_duplicate() -> void: print("Testgruppe: Strich auf der Spiegelachse dupliziert nicht (Achse X/Z)") var cases := [ ["Achse X", SymmetryCore.Mode.AXIS_X, Vector2(0.0, -80.0)], ["Achse Z", SymmetryCore.Mode.AXIS_Z, Vector2(80.0, 0.0)], ] for case in cases: var sym_name: String = case[0] var sym: int = case[1] var at: Vector2 = case[2] var parts := _make_tool(sym) var ttool = parts[0] var doc: Dictionary = parts[3] _stroke(ttool, at.x, at.y, TerrainField.Mode.RAISE, STROKE_SEC) var steps := int(ceil(STROKE_SEC / 0.05)) var ref := _reference_heights(steps, at.x, at.y, TerrainField.Mode.RAISE) var ref_b64 := TerrainField.encode(ref, HMIN, HMAX) _check(str(doc["terrain"]["heightmap_r16_b64"]) == ref_b64, "%s: Strich auf der Achse byte-identisch zu EINER Anwendung" % sym_name) var heights := _decode_heights(doc) _check(_count_asym_heights(heights, sym) == 0, "%s: Hälften exakt zellgleich" % sym_name) ttool.free() ## 3) Strich am Rand: gespiegelte Randzellen exakt, gegenüberliegende Ecke ## bekommt den Zwilling, kein Absturz, kein Duplikat-Effekt. func _test_edge_stroke() -> void: print("Testgruppe: Strich am Rand (Ecke, Punkt-Modus)") var parts := _make_tool(SymmetryCore.Mode.POINT) var ttool = parts[0] var doc: Dictionary = parts[3] _stroke(ttool, 150.0, 150.0, TerrainField.Mode.RAISE, STROKE_SEC) var heights := _decode_heights(doc) _check(_count_asym_heights(heights, SymmetryCore.Mode.POINT) == 0, "Rand-Strich: beide Hälften exakt zellgleich (auch Randzellen)") var corner := heights[(RES - 1) * RES + (RES - 1)] # +X/+Z-Ecke var opposite := heights[0] # −X/−Z-Ecke (Zwilling) _check(corner > 0.5 and absf(corner - opposite) < 0.0001, "Rand: Ecke %.3f m, Zwilling-Ecke %.3f m (identisch)" % [corner, opposite]) _check(absf(heights[(RES / 2) * RES + (RES / 2)]) < 0.0001, "Rand: Rastermitte unberührt (Fußabdruck reicht nicht hin)") ttool.free() ## 7) Fußabdruck ÜBERLAPPT die Mitte (Strich nahe dran): die strichnahe Seite ## gewinnt, das Ergebnis bleibt exakt zellgleich — auch am Selbstabbild- ## Punkt der ungeraden Rastermitte. func _test_overlap_stroke_near_center() -> void: print("Testgruppe: Überlapp-Strich nahe der Mitte (alle 3 Modi)") var cases := [ ["Punkt", SymmetryCore.Mode.POINT, Vector2(12.0, 12.0)], ["Achse X", SymmetryCore.Mode.AXIS_X, Vector2(12.0, -90.0)], ["Achse Z", SymmetryCore.Mode.AXIS_Z, Vector2(-90.0, 12.0)], ] for case in cases: var sym_name: String = case[0] var sym: int = case[1] var at: Vector2 = case[2] var parts := _make_tool(sym) var ttool = parts[0] var doc: Dictionary = parts[3] _stroke(ttool, at.x, at.y, TerrainField.Mode.RAISE, STROKE_SEC) var heights := _decode_heights(doc) _check(_count_asym_heights(heights, sym) == 0, "%s: Überlapp-Fußabdruck — Hälften exakt zellgleich" % sym_name) # Strich-Seite ist Master: die Zelle unter dem Cursor ist sichtbar # gehoben UND trägt denselben Wert wie ihr Zwilling. var under := _world_to_cell(at) var mc: Vector2i = SymmetryCore.mirror_cell(sym, under.x, under.y, RES) _check(heights[under.y * RES + under.x] > 0.5 and heights[under.y * RES + under.x] == heights[mc.y * RES + mc.x], "%s: Strich-Zelle und Zwilling identisch (%.3f m)" % [ sym_name, heights[under.y * RES + under.x]]) ttool.free() ## 4) Modus AUS = Verhalten wie vorher: byte-identisch zur direkten ## TerrainField-Anwendung (Gegenprobe), NICHTS wird gespiegelt. func _test_off_mode_unchanged() -> void: print("Testgruppe: Modus AUS = Verhalten wie vorher (Gegenprobe)") # Höhen-Strich: var parts := _make_tool(SymmetryCore.Mode.OFF) var ttool = parts[0] var doc: Dictionary = parts[3] _stroke(ttool, -100.0, -60.0, TerrainField.Mode.RAISE, STROKE_SEC) var steps := int(ceil(STROKE_SEC / 0.05)) var ref := _reference_heights(steps, -100.0, -60.0, TerrainField.Mode.RAISE) var ref_b64 := TerrainField.encode(ref, HMIN, HMAX) _check(str(doc["terrain"]["heightmap_r16_b64"]) == ref_b64, "AUS/Heben: byte-identisch zur direkten TerrainField-Anwendung") var heights := _decode_heights(doc) var twin := SymmetryCore.mirror_point(SymmetryCore.Mode.POINT, Vector3(-100.0, 0.0, -60.0), Vector2.ZERO) var twin_h := TerrainField.height_at(heights, RES, SIZE, SIZE, twin.x, twin.z) _check(absf(twin_h) < 0.0001, "AUS/Heben: Zwilling-Position bleibt FLACH (nichts gespiegelt)") ttool.free() # Malen-Strich: var parts_p := _make_tool(SymmetryCore.Mode.OFF) var ttool_p = parts_p[0] var doc_p: Dictionary = parts_p[3] ttool_p.call("set_paint_material", 1) # Sand statt Default-Gras _stroke(ttool_p, -100.0, -60.0, TerrainField.Mode.PAINT, 0.3) var ref_mats := PackedByteArray() ref_mats.resize(RES * RES) for i in int(ceil(0.3 / 0.05)): TerrainField.apply_material_brush(ref_mats, RES, SIZE, SIZE, -100.0, -60.0, RADIUS, 1, float(ttool_p.get("brush_weichheit"))) var ref_mats_b64 := SvmapIO.encode_materials(ref_mats) _check(str(doc_p["terrain"]["materials_r8_b64"]) == ref_mats_b64, "AUS/Malen: Material-Schicht byte-identisch zur direkten Anwendung") var got_mats := _decode_mats(doc_p) var twin_cell := _world_to_cell(Vector2(100.0, 60.0)) _check(int(got_mats[twin_cell.y * RES + twin_cell.x]) == 0, "AUS/Malen: Zwilling-Position bleibt Gras (nichts gespiegelt)") ttool_p.free() ## 5) Live-Rebuild-Pfad: was _process pro Frame an update_terrain gibt ## (Höhen UND Material-Schicht), ist bereits gespiegelt — der Live-Mesh- ## Input ist identisch zum Vollrebuild (Falle t_55ed041a bleibt zu). func _test_live_rebuild_inputs() -> void: print("Testgruppe: Live-Pfad — Mesh-Inputs pro Frame gespiegelt + Schicht dabei") var parts := _make_tool(SymmetryCore.Mode.POINT) var ttool = parts[0] var tmain = parts[1] var builder = parts[2] ttool.set("_cursor_valid", true) ttool.set("_cursor", Vector3(-100.0, 0.0, -60.0)) ttool.call("_begin_stroke", TerrainField.Mode.RAISE) for frame in 3: ttool.call("_process", 0.016) var live: PackedFloat32Array = builder.get("last_heights") var live_mats: PackedByteArray = builder.get("last_mats") _check(live.size() == RES * RES, "Live-Frame %d: Höhen-Feld komplett (33²)" % (frame + 1)) _check(_count_asym_heights(live, SymmetryCore.Mode.POINT) == 0, "Live-Frame %d: Mesh-Input-Höhen bereits gespiegelt" % (frame + 1)) _check(live_mats.size() == RES * RES, "Live-Frame %d: Material-Schicht MITGEBEN (t_55ed041a-Vertrag)" % (frame + 1)) # Malen live: auch der Mal-Frame gibt Höhe + (gespiegelte) Schicht weiter. ttool.call("_end_stroke") ttool.call("set_paint_material", 1) # Sand — Gras über Gras wäre ein No-op ttool.set("_cursor", Vector3(-100.0, 0.0, -60.0)) ttool.call("_begin_stroke", TerrainField.Mode.PAINT) ttool.call("_process", 0.016) var paint_mats: PackedByteArray = builder.get("last_mats") _check(_count_asym_mats(paint_mats, SymmetryCore.Mode.POINT) == 0, "Live-Mal-Frame: Mesh-Input-Material bereits gespiegelt") # Live-Splat-Sub-Rect (t_7a0a6b79): das Rechteck muss Mal-Zelle UND die # gespiegelte Schwesterzelle abdecken (der Sub-Rect-Refresh leitet genau # diese Region neu ab — sonst bliebe die Spiegelseite optisch alt). var splat_rect: Rect2i = builder.get("last_splat_rect") var painted_cell := _world_to_cell(Vector2(-100.0, -60.0)) var mirrored_cell := SymmetryCore.mirror_cell( SymmetryCore.Mode.POINT, painted_cell.x, painted_cell.y, RES) _check(splat_rect.size.x > 0 and splat_rect.has_point(Vector2i(painted_cell.x, painted_cell.y)) and splat_rect.has_point(Vector2i(mirrored_cell.x, mirrored_cell.y)), "Live-Mal-Frame: Splat-Sub-Rect deckt Mal-Zelle (%d,%d) + Spiegelzelle (%d,%d)" % [ painted_cell.x, painted_cell.y, mirrored_cell.x, mirrored_cell.y]) ttool.call("_end_stroke") _check(int(tmain.get("commits")) == 2, "Zwei Striche → zwei Commits (Undo-Vertrag bleibt)") ttool.free() # builder/tmain sind RefCounted (wie in test_terrain.gd) — kein free(). ## 6) Ein gespiegelter Strich = EIN Undo-Schritt; Undo nimmt BEIDE Hälften ## zurück, Redo bringt sie byte-identisch zurück. func _test_undo_one_step() -> void: print("Testgruppe: Gespiegelter Strich = EIN Undo-Schritt (beide Hälften)") var parts := _make_tool(SymmetryCore.Mode.POINT) var ttool = parts[0] var tmain = parts[1] var doc: Dictionary = parts[3] _stroke(ttool, -100.0, -60.0, TerrainField.Mode.RAISE, STROKE_SEC) _check(int(tmain.get("commits")) == 1, "Ein Strich → EIN Commit") var hist: ActionHistory = tmain.get("history") _check(hist.entries.size() == 1, "Ein Strich → EIN History-Eintrag (beide Hälften)") var raised_b64 := str(doc["terrain"]["heightmap_r16_b64"]) var desc := hist.undo() _check(desc.contains("Boden geformt"), "Undo-Desc nennt den Strich (%s)" % desc) var restored := _decode_heights(doc) var flat := true for v in restored: if v != 0.0: flat = false break _check(flat, "Undo: BEIDE Hälften wieder flach (ganzer gespiegelter Strich)") var redo_desc := hist.redo() _check(redo_desc.contains("Boden geformt"), "Redo-Desc stimmt") _check(str(doc["terrain"]["heightmap_r16_b64"]) == raised_b64, "Redo: byte-identisches gespiegeltes Höhenfeld zurück") ttool.free() ## Welt-X/Z → Rasterzelle (gleiche Konvention wie terrain_tool._world_to_grid). func _world_to_cell(at: Vector2) -> Vector2i: var fx: int = clampi(int(roundf((at.x / SIZE + 0.5) * float(RES - 1))), 0, RES - 1) var fz: int = clampi(int(roundf((at.y / SIZE + 0.5) * float(RES - 1))), 0, RES - 1) return Vector2i(fx, fz)