##[br] Watches a TileSetAtlasSource for changes. ##[br] Causes its 'parent' TileSetWatcher to emit terrains_changed when the atlas changes. ##[br] Also emits parent.atlas_autotiled when it thinks the user auto-generated atlas tiles. class_name AtlasWatcher ## Prevents the number of seen atlases from extending to infinity. const UNDO_LIMIT = 1024 ## Stores all of the atlas instance id's that have been seen before, to prevent autogen on redo. static var _registered_atlases := [] ## The TileSetWatcher that created this AtlasWatcher. Used to send signals back. var parent: TileSetWatcher ## The Source ID of `self.atlas`. var sid: int ## The atlas to be watched for changes. var atlas: TileSetAtlasSource func _init(parent: TileSetWatcher, sid: int, atlas: TileSetAtlasSource) -> void: self.parent = parent self.sid = sid self.atlas = atlas atlas.changed.connect(_atlas_changed, ConnectFlags.CONNECT_DEFERRED) var id := atlas.get_instance_id() # should not autogen if atlas was created through redo, i.e. its instance id already existed if _atlas_is_empty() and id not in _registered_atlases: _registered_atlases.push_back(id) if _registered_atlases.size() > UNDO_LIMIT: _registered_atlases.pop_front() atlas.changed.connect(_detect_autogen, ConnectFlags.CONNECT_DEFERRED | ConnectFlags.CONNECT_ONE_SHOT) func _atlas_is_empty() -> bool: return atlas.get_tiles_count() == 0 ## Returns true if the texture has any opaque pixels in the specified tile coordinates. func _is_opaque_tile(image: Image, tile: Vector2i, p_threshold: float = 0.1) -> bool: # We cannot use atlas.get_tile_texture_region(tile) as it fails on unregistered tiles. var region := Rect2i(tile * atlas.texture_region_size, atlas.texture_region_size) var sprite := image.get_region(region) if sprite.is_invisible(): return false # We're still not sure if the tile is empty or not. # Godot's auto-gen considers 0.1 opacity as "transparent" but not "invisible". for y in range(region.position.y, region.end.y): for x in range(region.position.x, region.end.x): if image.get_pixel(x, y).a > p_threshold: return true return false ##[br] HACK: literally just tries to guess which tiles the terrain autogen system would make ##[br] Called once, and only once, at the end of the first frame that a texture is created. func _detect_autogen() -> void: var size := Vector2i(atlas.texture.get_size()) / atlas.texture_region_size var image := atlas.texture.get_image() var expected_tiles := [] for y in size.y: for x in size.x: var tile := Vector2i(x, y) if atlas.has_tile(tile) != _is_opaque_tile(image, tile): return parent.atlas_autotiled.emit(sid, atlas) ## Called every time the atlas changes. Simply flags that terrains have changed. func _atlas_changed() -> void: parent._flag_terrains_changed = true