straywild/drawing/surface_drawing_cell_buffer.gd

284 lines
7.9 KiB
GDScript

# 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