@tool extends CanvasGroup class_name MercatorTileMap const VECTOR00: = Vector2(0, 0) const VECTOR11: = Vector2(1, 1) ## Отображение карт WEB Mercator #http://10.1.1.220/osm_tiles/0/0/0.png #http://169.254.27.71:8001/osm_tiles/{z}/{x}/{y}.png #https://tile.openstreetmap.org/{z}/{x}/{y}.png @export var base_url: String = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png': set(v): if base_url != v: base_url = v _clean_all() queue_redraw() const TILE_REQUEST_NAME: = 'tile_request_%d' @export var zoom_step: float = 0.1 @export var tile_width: float = 256.0 @export var tile_height: float = 256.0 @export_range(1, 1000, 1) var max_concurrent_requests:int = 5 @export_range(100, 100000, 1) var max_cached_tiles: int = 1024 @export_range(0, 20, 1) var max_zoom_level: int = 20 @export_range(0, 20, 1) var min_zoom_level: int = 0 @export_range(0, 20, 1) var max_zoom_level_server: int = 20 @export_color_no_alpha() var back_color: = Color.BLACK @export var show_ship_radius: bool = true @export var ship_radius_km: float = 8.0 @onready var coordinates_label: String @onready var threat_mark: PackedScene = load('res://scenes/tilemap/threat.tscn') @onready var ship_mark: PackedScene = load('res://scenes/tilemap/ship-mark.tscn') var ship_id := 9999 var ship_instance: Node2D = null const default_size: = 400.0 var is_centered: = false var is_out_border1: = true var is_out_border2: = true var threat_markers := {} var zoom_timer: Timer var zoom_action: String @export var animation_time: float = 0.1: set(val): animation_time = val @export var size: Vector2i = Vector2i(default_size, default_size): set(v): $canvas.size = v _on_resize() queue_redraw() get: return $canvas.size @export var ax: float = 0.0: set(v): _xyz.x = v queue_redraw() get: return _xyz.x @export var ay: float = 0.0: set(v): _xyz.y = v queue_redraw() get: return _xyz.y @export var az: float = 0.0: set(v): _xyz.z = v queue_redraw() get: return _xyz.z var _xyz: Vector3 = Vector3(0, 0, 0) var _cache: Dictionary = {} var _queue: Dictionary = {} var _dragging: bool = false var _drag_pos: Vector2 = Vector2.ZERO var _last_cache_check: float = 0 var _rollover: bool = false var _marks: Dictionary = {} var _req_count: = 0 var image: = Image.new() var image_load_proc: Callable var now: = 0.0 var visible_marks_count: = 0 var border1: ImageTexture var border2: ImageTexture ## Вызывается после того, как загрузка плиток завершена signal load_completed() ## Вызывается при запуске загрузки плиток signal load_started() ## Вызывается при получении ответа от сервера карт signal map_server_online() ## Вызывается при отсутствии ответа от сервера карт signal map_server_offline() const image_load_proc_names = { 'png': 'load_png_from_buffer', 'jpg': 'load_jpg_from_buffer', 'tga': 'load_tga_from_buffer', 'webp': 'load_webp_from_buffer' } @export_enum('png', 'jpg', 'jpeg', 'tga', 'webp') var image_type: String = 'png': set(v): image_load_proc = Callable(image, image_load_proc_names[v]) func on_map_centered(toggle_centered: bool): is_centered = toggle_centered func get_marks_count(): return _marks.size() func _ready() -> void: var img: = Image.create(1, 2, false, Image.FORMAT_RGBA8) img.set_pixel(0, 0, Color.CORAL) img.set_pixel(0, 1, Color(1, 1, 1, 0)) border1 = ImageTexture.create_from_image(img) img.set_pixel(0, 0, Color(1, 1, 1, 0)) img.set_pixel(0, 1, Color.CORAL) border2 = ImageTexture.create_from_image(img) zoom_timer = Timer.new() add_child(zoom_timer) zoom_timer.one_shot = false zoom_timer.wait_time = 0.1 zoom_timer.timeout.connect(_on_zoom_timeout) _xyz.z = min_zoom_level image_type = 'png' $canvas.connect('mouse_entered', func(): _rollover = true) $canvas.connect('mouse_exited', func(): _rollover = false) _on_resize() signaller.connect('map_centered', on_map_centered) if not Engine.is_editor_hint(): var unit_nav = network.get_unit_instance('навигация') unit_nav.connect('data_received', on_data_received) ProjectSettings.connect('settings_changed', _settings_changed) threats.connect('threat_new', Callable(self, '_on_threat_new')) threats.connect('threat_lost', Callable(self, '_on_threat_lost')) threats.connect('threat_update', Callable(self, '_on_threat_update')) func _settings_changed(): base_url = ProjectSettings.get_setting('application/config/Шаблон адреса сервера карт', base_url) func on_data_received(coords: Dictionary) -> void: if not is_centered: return if coords.has_all(['latitude', 'longitude']): set_coordinates(coords['longitude'], coords['latitude'], coords['k']) func _on_resize() -> void: var center: Vector2 = $canvas.size / 2.0 $canvas/spr_mask.position = center $canvas/spr_mask.scale = $canvas.size / default_size ## Создаёт новую отметку из координат экрана func add_mark_from_screen_pos(id: int, scr_xy: Vector2, mark: Node) -> bool: if _marks.has(id): return false var wrld_pos: Vector2 = screen_to_world(_xyz, scr_xy) _marks[id] = { 'position': wrld_pos, 'mark': mark, 'gap_size': 0.0 } add_child(mark) return true func delete_mark(id: int) -> bool: if not _marks.has(id): return false _marks[id].mark.queue_free() _marks.erase(id) queue_redraw() return true ## Создаёт новую отметку из долготы и широты func add_mark_from_lon_lat(id: int, lon: float, lat: float, mark: Node, gap_size: float = 32) -> bool: if _marks.has(id): return false var wrld_pos: Vector2 = lonlat_to_world(lon, lat) _marks[id] = {'position': wrld_pos, 'mark': mark, 'gap_size': gap_size} add_child(mark) queue_redraw() return true ## Возвращает новую позицию для точки, если она выходит из поля видимости func fix_mark_position(dot) -> void: if is_mark_visible(dot): return var v = 2 * tile_width * pow(2, _xyz.z) if dot.position.x < _xyz.x - $canvas.size.x / v: dot.position.x = _xyz.x - $canvas.size.x / v elif dot.position.x > _xyz.x + $canvas.size.x / v: dot.position.x = _xyz.x + $canvas.size.x / v func is_mark_visible(dot) -> bool: var screen_pos = world_to_screen(dot.position) return screen_pos.x > 0 and screen_pos.x < $canvas.size.x func _input(e) -> void: var lmp = get_local_mouse_position() if not _rollover: _dragging = false return if e is InputEventMouseButton: if e.pressed: if e.button_index == MOUSE_BUTTON_WHEEL_DOWN: if (not is_out_border1) and (not is_out_border2): increment_zoom(-zoom_step, lmp) update_scale_label() queue_redraw() elif e.button_index == MOUSE_BUTTON_WHEEL_UP: increment_zoom( zoom_step, lmp) update_scale_label() queue_redraw() if e.button_index == MOUSE_BUTTON_LEFT: _dragging = e.pressed _drag_pos = screen_to_world(_xyz, lmp) elif e is InputEventMouseMotion: if _dragging: var wp = screen_to_world(_xyz, lmp) var diff = _drag_pos - wp _xyz.x += diff.x if (not is_out_border1) and diff.y >= 0: _xyz.y += diff.y if is_out_border1 and diff.y <= 0: _xyz.y += diff.y if is_out_border2 and diff.y >= 0: _xyz.y += diff.y if (not is_out_border2) and diff.y <= 0: _xyz.y += diff.y queue_redraw() if diff.length() > 0: signaller.emmit(signaller.map_user_panning) func increment_zoom(increment: float, pivot: Vector2) -> void: var p1: = screen_to_world(_xyz, pivot) _xyz.z = clampf(_xyz.z + increment, min_zoom_level, max_zoom_level) var p2: = screen_to_world(_xyz, pivot) _xyz.x -= p2.x - p1.x _xyz.y -= p2.y - p1.y func set_zoom(zoom: float, pivot: Vector2) -> void: var p1: = screen_to_world(_xyz, pivot) _xyz.z = clampf(zoom, max_zoom_level, min_zoom_level) var p2: = screen_to_world(_xyz, pivot) _xyz.x -= p2.x - p1.x _xyz.y -= p2.y - p1.y func _draw() -> void: # Рисование плиток var z: float = min(max_zoom_level, _xyz.z) var t1: = screen_to_tile(VECTOR00, z) var t2: = screen_to_tile($canvas.size, z) + VECTOR11 is_out_border1 = true is_out_border2 = true for tx in range(t1.x, t2.x): for ty in range(t1.y, t2.y): _draw_tile(tx, ty, z) # Рисование отметок var n: = pow(2, z) var dx: = n * tile_width var c: Vector2 = $canvas.size / 2.0 var r: float = c.length() visible_marks_count = 0 for dot in _marks.values(): var scr_pos: Vector2 = world_to_screen(dot.position) scr_pos.x = posmod(int(scr_pos.x), int(dx)) dot.mark.position = scr_pos dot.mark.visible = is_mark_visible(dot) dot.mark.visible = c.distance_to(scr_pos) < (r - dot.gap_size) visible_marks_count += int(dot.mark.visible) # Рисование границ var p1: Vector2 = $canvas.position var p2: Vector2 = $canvas.size / Vector2(1, 20) if is_out_border1 and _dragging: draw_texture_rect(border1, Rect2(p1, p2), false, Color.WHITE, false) p1.y = p1.y + $canvas.size.y - p2.y if is_out_border2 and _dragging: draw_texture_rect(border2, Rect2(p1, p2), false, Color.WHITE, false) if should_draw_ship_radius(): draw_ship_radius() func _clean_all() -> void: _queue.clear() _cache.clear() _req_count = 0 # get the tile from queue with the newest timestamp func get_next_in_queue() -> Tile: var tile = null for idx in _queue: var t = _queue.get(idx) if not tile or t.t > tile.t: tile = t return tile func _clean_cache() -> void: var overflow = _cache.size() - max_cached_tiles if overflow <= 0: return var vals = _cache.values() vals.sort_custom(func(t1, t2): return t1.t < t2.t) for i in overflow: _cache.erase(vals[i].i) func _process(delta) -> void: now += delta # Если кэш переполнен - очистить его. if not _last_cache_check or now - _last_cache_check > 5: _last_cache_check = now _clean_cache() while not _queue.is_empty() and (_req_count < max_concurrent_requests): var tile = get_next_in_queue() if not tile: break var req = HTTPRequest.new() _req_count += 1 req.set_meta('tile', tile) add_child(req) req.name = NodePath(TILE_REQUEST_NAME % tile.i) _queue.erase(tile.i) req.request_completed.connect(_response.bind(req, tile)) if req.request(tile.url) != OK: tile.t = now remove_child(req) req.queue_free() _req_count -= 1 push_error('запрос \"%s\" не отправлен' % tile.url) if _queue.is_empty(): emit_signal('load_completed') func process_online(result): # ERROR: res://scenes/tilemap/tilemap.gd:396 - Invalid access to property or key 'online' on a base object of type 'Nil'. var unit_instance: = network.get_unit_instance('уарэп-карта') if not unit_instance: return true if result != HTTPRequest.RESULT_SUCCESS: if unit_instance.online: unit_instance.online = false unit_instance.emit_signal('line_changed', unit_instance) return false unit_instance.rx_tick = Time.get_ticks_msec() if not unit_instance.online: unit_instance.online = true unit_instance.emit_signal('line_changed', unit_instance) return true func _response(result, code, _headers: Array, body: PackedByteArray, req, tile: Tile) -> void: _req_count -= 1 remove_child(req) req.queue_free() if not process_online(result): _queue[tile.i] = tile return if code == 404: push_error('на сервере нет \"%s\"' % tile.url) return if Error.OK != image_load_proc.call(body): push_error('не удалось получить изображение (неверный формат?) из \"%s\"' % tile.url) return tile.texture = ImageTexture.create_from_image(image) _cache[tile.i] = tile queue_redraw() func _draw_subtile(tx: int, ty: int, tz: int, origx: int, origy: int, origz: float) -> bool: var subtile = get_tile(tx, ty, tz) if not subtile: return false var p1 = tile_to_screen(origx, origy, origz) var p2 = tile_to_screen(origx + 1, origy + 1, origz) var x1 = tile_to_screen(tx, ty, tz) var x2 = tile_to_screen(tx + 1, ty + 1, tz) var xdiff = x2.x - x1.x var xrat1 = (p1.x - x1.x) / xdiff var xrat2 = (p2.x - x1.x) / xdiff var xwidth = xrat2 - xrat1 var ydiff = x2.y - x1.y var yrat1 = (p1.y - x1.y) / ydiff var yrat2 = (p2.y - x1.y) / ydiff var yheight = yrat2 - yrat1 var rect = Rect2(xrat1 * tile_width, yrat1 * tile_height, xwidth * tile_width, yheight * tile_height) if subtile.texture: check_borders(p1, p2, $canvas.size.y) draw_texture_rect_region(subtile.texture, Rect2(p1, p2 - p1), rect) return true return false func check_borders(p1, p2, h): if p1.y < 0: is_out_border1 = false if p1.y + (p2 - p1).y > h: is_out_border2 = false func _draw_tile(tx: int, ty: int, z: float) -> void: var tz: = int(floor(z)) var p1: = tile_to_screen(tx, ty, z) var p2: = tile_to_screen(tx + 1, ty + 1, z) var tile: = get_tile(tx, ty, tz) if tile: if tile.texture: check_borders(p1, p2, $canvas.size.y) draw_texture_rect(tile.texture, Rect2(p1, p2 - p1), false, Color.WHITE, false) else: var zzz: = tz var txx: = tx var tyy: = ty while zzz > 1: zzz -= 1 txx = floor(txx / 2.0) tyy = floor(tyy / 2.0) if _draw_subtile(txx, tyy, zzz, tx, ty, z): break ## convert lon/lat to world coords func lonlat_to_world(lon: float, lat: float) -> Vector2: var x: = lon / 180.0 var latsin: = sin(deg_to_rad(lat) * sign(lat)) var y: float = (sign(lat) * (log((1.0 + latsin) / (1.0 - latsin)) / 2.0)) / PI return Vector2(x, y) ## convert lon/lat to screen coords func lonlat_to_screen(lon: float, lat: float) -> Vector2: var w: Vector2 = lonlat_to_world(lon, lat) return world_to_screen(w) ## convert lon/lat to tile coords func lonlat_to_tile(lon: float, lat: float, z: float) -> Vector2: var w: Vector2 = lonlat_to_world(lon,lat) return world_to_tile(w.x, w.y, z) ## convert world coords to lon/lat func world_to_lonlat(wx: float, wy: float) -> Vector2: var lon: float = wx * 180.0 var lat: float = rad_to_deg(atan(sinh(wy * PI))) return Vector2(lon, lat) ## convert screen coords to lon/lat func screen_to_lonlat(scr_xy: Vector2) -> Vector2: var w: Vector2 = screen_to_world(_xyz, scr_xy) return world_to_lonlat(w.x, w.y) ## convert tile coords to lon/lat func tile_to_lonlat(tx: float, ty: float, tz: float) -> Vector2: var w: Vector2 = tile_to_world(tx, ty, tz) return world_to_lonlat(w.x, w.y) ## convert screen coords to world coords func screen_to_world(xyz: Vector3, s: Vector2) -> Vector2: var n: float = pow(2.0, xyz.z) var span_w: float = n * tile_width var span_h: float = n * tile_height var px: float = s.x - $canvas.size.x / 2 + span_w / 2 var py: float = s.y - $canvas.size.y / 2 + span_h / 2 var xr: float = px / span_w var yr: float = py / span_h var x: float = (xr * 2.0 - 1.0) + xyz.x var y: float = ((-yr * 2.0) + 1.0) + xyz.y return Vector2(x, y) ## convert screen coords to tile coords func screen_to_tile(scr_xy: Vector2, z: float) -> Vector2: var world: Vector2 = screen_to_world(_xyz, scr_xy) return world_to_tile(world.x, world.y, z) ## convert tile coords to screen coords func tile_to_screen(tx: float, ty: float, tz: float) -> Vector2: var w: Vector2 = tile_to_world(tx, ty, tz) return world_to_screen(w) ## convert tile coords to world coords func tile_to_world(tx: float, ty: float, tz: float) -> Vector2: var n: float = pow(2.0, floor(tz)) var x: float = (tx / n) * 2.0 - 1.0 var y: float = -((ty / n) * 2.0 - 1.0) return Vector2(x, y) ## convert world coords to tile coords func world_to_tile(wx: float, wy: float, z: float) -> Vector2: var n: float = pow(2.0, floor(z)) var tx: float = floor(((wx + 1.0) / 2.0) * n) var ty: float = floor(((-wy + 1.0) / 2.0) * n) return Vector2(tx, ty) ## convert world coords to screen coords func world_to_screen(pos: Vector2) -> Vector2: var n: = pow(2.0, _xyz.z) var wh: = Vector2(n * tile_width, n * tile_height) var rxy: = Vector2(((pos.x - _xyz.x) + 1.0) / 2.0, (-(pos.y - _xyz.y) + 1.0) / 2.0) return wh * rxy + ($canvas.size - wh) / 2.0 ## get the tile index from xyz tile coords func xyz_to_idx(x: float, y: float, z: float) -> int: var i: = (pow(4.0, z) - 1.0) / 3.0 var n: = pow(2.0, z) return int(i + (y * n + x)) func get_tile(x: int, y: int, z: int) -> Tile: var n: float = pow(2, z) if x >= 0: x %= int(n) else: x = -x - 1 x = int(n - 1) - (x % int(n)) if z < 0 or y < 0 or y >= n: return null var idx: int = xyz_to_idx(x, y, z) var tile = null var req: = get_node_or_null(NodePath(TILE_REQUEST_NAME % idx)) if req: tile = req.get_meta('tile') if tile != null: tile.t = now return tile tile = _cache.get(idx) if tile: tile.t = now return tile tile = _queue.get(idx) if tile: tile.t = now return tile var queue_size: = _queue.size() tile = Tile.new(idx, x, y, z) tile.url = base_url.replace('{x}', str(x)).replace('{y}', str(y)).replace('{z}', str(z)) tile.t = now if z <= max_zoom_level_server: _queue[idx] = tile if queue_size == 0 and _queue.size(): emit_signal('load_started') return tile func lonlat_to_dms(lon: float, lat: float) -> String: var pf: String = 'N' if lat >= 0 else 'S' lat = abs(lat) var deg: float = floor(lat) var minute: float = (lat - deg) * 60 var second: float = (minute - floor(minute)) * 60 var text: String = ('%02d' % deg) + ('%02d' % floor(minute)) + ('%02d' % floor(second)) + pf + ' ' pf = 'E' if lon >= 0 else 'W' lon = abs(lon) deg = floor(lon) minute = (lon - deg) * 60 second = (minute - floor(minute)) * 60 text += ('%02d' % deg) + ('%02d' % floor(minute)) + ('%02d' % floor(second)) + pf return text class Tile: var i: int = 0 var x: int = 0 var y: int = 0 var z: int = 0 var t: float = 0.0 var url: String = '' var texture: Texture2D = null func _init(i0: int, x0: int, y0: int, z0: int): self.i = i0 self.x = x0 self.y = y0 self.z = z0 # Возвращает расстояние в метрах между двумя точками на карте func haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float: # Радиус Земли в метрах const R: = 6372795.0 # Преобразование градусов в радианы var rlat1: = deg_to_rad(lat1) var rlon1: = deg_to_rad(lon1) var rlat2: = deg_to_rad(lat2) var rlon2: = deg_to_rad(lon2) # вычисление длины большого круга var cl1: = cos(rlat1) var cl2: = cos(rlat2) var sl1: = sin(rlat1) var sl2: = sin(rlat2) var delta: = rlon2 - rlon1 var cdelta: = cos(delta) var sdelta: = sin(delta) var y: = sqrt(pow(cl2 * sdelta, 2.0) + pow(cl1 * sl2 - sl1 * cl2 * cdelta, 2.0)) var x: = sl1 * sl2 + cl1 * cl2 * cdelta var ad: = atan2(y, x) var dist: = ad * R return dist func remove_closest_dot(screen_pos: Vector2) -> bool: var closest_dot = find_at_screen_position(screen_pos) if not closest_dot: return false remove_child(closest_dot.mark) closest_dot.mark.queue_free() _marks.erase(closest_dot) return true func find_at_screen_position(screen_pos: Vector2) -> Node: var closest_dot = null var closest_distance = INF for mark in _marks.values(): var dot_screen_pos = world_to_screen(mark.position) var distance = dot_screen_pos.distance_to(screen_pos) if distance < closest_distance: closest_distance = distance closest_dot = mark return closest_dot func clear() -> void: _clean_all() queue_redraw() func set_coordinates(lon: float, lat: float, course: float) -> void: var anim: Animation = $player.get_animation('goto') var new_pos: Vector2 = lonlat_to_world(lon, lat) anim.track_insert_key(0, 0, _xyz.x) anim.track_insert_key(1, 0, _xyz.y) anim.track_insert_key(0, animation_time, new_pos.x) anim.track_insert_key(1, animation_time, new_pos.y) $player.play('goto') update_coordinates_label(lon, lat) if not _marks.has(ship_id): ship_instance = ship_mark.instantiate() add_mark_from_lon_lat(ship_id, lon, lat, ship_instance, 0.0) else: _marks[ship_id].position = lonlat_to_world(lon, lat) ship_instance.rotation_degrees = fmod(course, 360.0) queue_redraw() func blend_mask(val: bool) -> void: $canvas/spr_mask.visible = val queue_redraw() func update_coordinates_label(lon: float, lat: float) -> void: coordinates_label = 'Координаты: NS:%f, EW:%f' % [lat, lon] signaller.emit_signal('update_coordinates_map', coordinates_label) func update_scale_label() -> void: var scale_label: = 'Масштаб: %f' % round(_xyz.z) signaller.emit_signal('update_scale_map', scale_label) func _on_threat_new(th) -> void: var bip_instance = threat_mark.instantiate() if bip_instance: bip_instance.name = str(th.id) bip_instance.position = lonlat_to_world(th.lon, th.lat) add_mark_from_lon_lat(th.id, th.lon, th.lat, bip_instance, 32) func _on_threat_update(th) -> void: var target = get_parent().get_node_or_null(str(th.id)) if not target: return var new_position = lonlat_to_world(th.lon, th.lat) if _marks.has(th.id): _marks[th.id].position = new_position if new_position != target.position: target.position = new_position func _on_threat_lost(th) -> void: var target = get_parent().get_node_or_null(str(th.id)) if target: target.queue_free() func zoom_in() -> void: if _xyz.z >= max_zoom_level: return var center: Vector2 = $canvas.size / 2.0 increment_zoom(zoom_step, center) update_scale_label() queue_redraw() func zoom_out() -> void: if _xyz.z <= min_zoom_level: return var center: Vector2 = $canvas.size / 2.0 increment_zoom(-zoom_step, center) update_scale_label() queue_redraw() func _on_zoom_plus_button_down() -> void: zoom_action = 'in' perform_zoom() zoom_timer.start(0.3) func _on_zoom_minus_button_down() -> void: zoom_action = 'out' perform_zoom() zoom_timer.start(0.3) func _on_zoom_button_up() -> void: zoom_action = '' zoom_timer.stop() func _on_zoom_timeout(): perform_zoom() zoom_timer.wait_time = 0.1 func perform_zoom(): if zoom_action != '': if zoom_action == 'in': zoom_in() else: zoom_out() update_scale_label() # Вычисление для радиуса в мировых координатах func get_ship_radius_world() -> float: if not _marks.has(ship_id): return 0.0 var ship_world_pos = _marks[ship_id].position var ship_lonlat = world_to_lonlat(ship_world_pos.x, ship_world_pos.y) var earth_radius_km = 6371.0 var distance_deg = (ship_radius_km / earth_radius_km) * (180.0 / PI) / cos(deg_to_rad(ship_lonlat.y)) var east_lon = ship_lonlat.x + distance_deg var east_point_world = lonlat_to_world(east_lon, ship_lonlat.y) return abs(east_point_world.x - ship_world_pos.x) func should_draw_ship_radius() -> bool: if not show_ship_radius or not _marks.has(ship_id): return false return _xyz.z >= 11 func draw_ship_radius() -> void: var ship_world_pos = _marks[ship_id].position var ship_screen_pos = world_to_screen(ship_world_pos) var radius_world = get_ship_radius_world() var radius_point_world = Vector2(ship_world_pos.x + radius_world, ship_world_pos.y) var radius_point_screen = world_to_screen(radius_point_world) var radius_screen = ship_screen_pos.distance_to(radius_point_screen) draw_circle(ship_screen_pos, radius_screen, Color.ORANGE_RED * Color(1, 1, 1, 0.1)) # отрисовка заливки