284 lines
7.9 KiB
GDScript
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
|