# SPDX-License-Identifier: GPL-3.0-or-later class_name SurfaceDrawingCellBuffer extends RefCounted const ENCODING := "palette_author_planes_v1" var width: int = 0 var height: int = 0 var _color_indices := PackedByteArray() var _author_indices := PackedInt32Array() var _participants: Array[String] = [] var _participant_slots: Dictionary[String, int] = {} var _author_masks: Dictionary[int, PackedByteArray] = {} var _painted_count: int = 0 func configure_from_state( data: Dictionary, already_validated: bool = false, ) -> bool: if not already_validated and not SurfaceDrawingProtocol.validate_canvas_state(data): return false width = int(data["width"]) height = int(data["height"]) _reset_planes() for value: Variant in data.get("participant_fingerprints", []): _add_participant(str(value)) _add_participant(str(data.get("creator_fingerprint", ""))) if str(data.get("cell_encoding", "")) == ENCODING: return _configure_packed( data.get("cell_colors", PackedByteArray()), data.get("cell_authors", PackedInt32Array()), ) for value: Variant in data.get("cells", []): if typeof(value) != TYPE_DICTIONARY or not _apply_edit(value): return false return true func apply_edits(edits: Array) -> bool: for value: Variant in edits: if typeof(value) != TYPE_DICTIONARY or not _apply_edit(value): return false return true func ensure_participants(values: Array) -> bool: for value: Variant in values: if _add_participant(str(value)) <= 0: return false return true func get_cell(x: int, y: int) -> Dictionary: var key: int = _key(x, y) if key < 0 or _color_indices[key] == 0: return {} var author_slot: int = _author_indices[key] return { "x": x, "y": y, "color_id": str( SurfaceDrawingPalette.id_for_palette_index(_color_indices[key]) ), "author_fingerprint": ( _participants[author_slot - 1] if author_slot > 0 and author_slot <= _participants.size() else "" ), } func get_cell_value(x: int, y: int) -> Dictionary: var cell: Dictionary = get_cell(x, y) if cell.is_empty(): return {} return { "color_id": cell["color_id"], "author_fingerprint": cell["author_fingerprint"], } func get_painted_count() -> int: return _painted_count func get_participants() -> Array[String]: return _participants.duplicate() func get_author_cell_keys(fingerprint: String) -> PackedInt32Array: var slot: int = int(_participant_slots.get(fingerprint, 0)) var mask: PackedByteArray = _author_masks.get(slot, PackedByteArray()) var result := PackedInt32Array() if slot <= 0 or mask.is_empty(): return result for key: int in width * height: if _mask_has(mask, key): result.append(key) return result func to_cells() -> Array[Dictionary]: var result: Array[Dictionary] = [] for key: int in width * height: if _color_indices[key] == 0: continue result.append(get_cell(key % width, floori(float(key) / float(width)))) return result func to_compact_fields() -> Dictionary: return { "cell_encoding": ENCODING, "cell_colors": _color_indices.duplicate(), "cell_authors": _author_indices.duplicate(), "participant_fingerprints": get_participants(), } func to_rgba8_data(hidden: bool = false) -> PackedByteArray: var result := PackedByteArray() result.resize(width * height * 4) # Match Image.fill(Color.TRANSPARENT): transparent texels retain white RGB, # preventing dark fringes if filtering is ever changed by a platform driver. for pixel_index: int in width * height: var offset: int = pixel_index * 4 result[offset] = 255 result[offset + 1] = 255 result[offset + 2] = 255 if hidden: return result for key: int in width * height: var palette_index: int = _color_indices[key] if palette_index == 0: continue var x: int = key % width var y: int = floori(float(key) / float(width)) var image_key: int = (height - 1 - y) * width + x var offset: int = image_key * 4 var color: Color = SurfaceDrawingPalette.color_for_palette_index( palette_index ) result[offset] = roundi(color.r * 255.0) result[offset + 1] = roundi(color.g * 255.0) result[offset + 2] = roundi(color.b * 255.0) result[offset + 3] = roundi(color.a * 255.0) return result func estimated_storage_bytes() -> int: var masks_size: int = 0 for mask: PackedByteArray in _author_masks.values(): masks_size += mask.size() return _color_indices.size() + _author_indices.size() * 4 + masks_size func _configure_packed(colors: Variant, authors: Variant) -> bool: if ( typeof(colors) != TYPE_PACKED_BYTE_ARRAY or typeof(authors) != TYPE_PACKED_INT32_ARRAY ): return false var packed_colors: PackedByteArray = colors var packed_authors: PackedInt32Array = authors if ( packed_colors.size() != width * height or packed_authors.size() != width * height ): return false _color_indices = packed_colors.duplicate() _author_indices = packed_authors.duplicate() _painted_count = 0 for key: int in width * height: var color_index: int = _color_indices[key] var author_slot: int = _author_indices[key] if color_index == 0: if author_slot != 0: return false continue if ( color_index > SurfaceDrawingPalette.COLORS.size() or author_slot <= 0 or author_slot > _participants.size() ): return false _painted_count += 1 _set_author_mask_bit(author_slot, key, true) return true func _apply_edit(value: Dictionary) -> bool: var x: int = int(value.get("x", -1)) var y: int = int(value.get("y", -1)) var key: int = _key(x, y) if key < 0: return false var color_id := StringName(str(value.get("color_id", ""))) var next_color_index: int = ( SurfaceDrawingPalette.palette_index_for_id(color_id) if not color_id.is_empty() else 0 ) if not color_id.is_empty() and next_color_index <= 0: return false var next_author_slot: int = 0 if next_color_index > 0: next_author_slot = _add_participant( str(value.get("author_fingerprint", "")) ) if next_author_slot <= 0: return false var previous_color_index: int = _color_indices[key] var previous_author_slot: int = _author_indices[key] if previous_color_index == next_color_index and previous_author_slot == next_author_slot: return true if previous_author_slot > 0: _set_author_mask_bit(previous_author_slot, key, false) if previous_color_index == 0 and next_color_index > 0: _painted_count += 1 elif previous_color_index > 0 and next_color_index == 0: _painted_count -= 1 _color_indices[key] = next_color_index _author_indices[key] = next_author_slot if next_author_slot > 0: _set_author_mask_bit(next_author_slot, key, true) return true func _reset_planes() -> void: _color_indices.resize(width * height) _color_indices.fill(0) _author_indices.resize(width * height) _author_indices.fill(0) _participants.clear() _participant_slots.clear() _author_masks.clear() _painted_count = 0 func _add_participant(fingerprint: String) -> int: var existing: int = int(_participant_slots.get(fingerprint, 0)) if existing > 0: return existing if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): return 0 if _participants.size() >= SurfaceDrawingProtocol.MAX_PARTICIPANTS: return 0 _participants.append(fingerprint) var slot: int = _participants.size() _participant_slots[fingerprint] = slot return slot func _set_author_mask_bit(slot: int, key: int, enabled: bool) -> void: if slot <= 0 or key < 0: return var mask: PackedByteArray = _author_masks.get(slot, PackedByteArray()) var required_size: int = ceili(float(width * height) / 8.0) if mask.size() != required_size: mask.resize(required_size) var byte_index: int = floori(float(key) / 8.0) var bit: int = 1 << (key % 8) if enabled: mask[byte_index] = mask[byte_index] | bit else: mask[byte_index] = mask[byte_index] & (~bit & 0xff) _author_masks[slot] = mask func _mask_has(mask: PackedByteArray, key: int) -> bool: var byte_index: int = floori(float(key) / 8.0) return ( byte_index >= 0 and byte_index < mask.size() and (mask[byte_index] & (1 << (key % 8))) != 0 ) func _key(x: int, y: int) -> int: if x < 0 or x >= width or y < 0 or y >= height: return -1 return y * width + x