Add artwork export and stamping
This commit is contained in:
parent
ba35db1015
commit
8107e2d89c
21 changed files with 1727 additions and 105 deletions
|
|
@ -2,7 +2,6 @@ class_name SurfaceDrawingCanvas
|
|||
extends Node3D
|
||||
|
||||
const GUIDED_PIXEL_FILL: float = 0.88
|
||||
const FINISHED_PIXEL_FILL: float = 1.0
|
||||
const SURFACE_OFFSET: float = 0.012
|
||||
const LAYER_OFFSET_STEP: float = 0.0002
|
||||
const SURFACE_SAMPLE_DEPTH: float = 0.45
|
||||
|
|
@ -13,13 +12,17 @@ var grid_height: int = 0
|
|||
var cell_size: float = 0.0
|
||||
var revision: int = -1
|
||||
var creator_fingerprint: String = ""
|
||||
var participant_fingerprints: Array[String] = []
|
||||
|
||||
var _surface_origin: Vector3
|
||||
var _surface_normal: Vector3
|
||||
var _surface_tangent: Vector3
|
||||
var _surface_bitangent: Vector3
|
||||
var _cells: Dictionary[int, Dictionary] = {}
|
||||
var _color_meshes: Dictionary[StringName, MultiMeshInstance3D] = {}
|
||||
var _cell_surface_transforms: Array[Transform3D] = []
|
||||
var _pixel_image: Image
|
||||
var _pixel_texture: ImageTexture
|
||||
var _pixel_instance: MeshInstance3D
|
||||
var _grid_instance: MeshInstance3D
|
||||
var _guide_visible: bool = true
|
||||
var _finalized: bool = false
|
||||
|
|
@ -42,6 +45,7 @@ func setup(
|
|||
cell_size = float(data["cell_size"])
|
||||
revision = int(data["revision"])
|
||||
creator_fingerprint = str(data["creator_fingerprint"])
|
||||
participant_fingerprints = _participants_from_state(data)
|
||||
_finalized = bool(data.get("finalized", false))
|
||||
_guide_visible = bool(data.get("guide_visible", true)) and not _finalized
|
||||
_layer = int(data.get("layer", 0))
|
||||
|
|
@ -65,7 +69,7 @@ func setup(
|
|||
var cell: Dictionary = cell_value
|
||||
_cells[_cell_key(int(cell["x"]), int(cell["y"]))] = cell.duplicate(true)
|
||||
_build_grid()
|
||||
_rebuild_pixels()
|
||||
_build_pixel_renderer()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -83,8 +87,15 @@ func apply_update(data: Dictionary) -> bool:
|
|||
_cells.erase(key)
|
||||
else:
|
||||
_cells[key] = edit.duplicate(true)
|
||||
participant_fingerprints = _participants_from_update(
|
||||
data, participant_fingerprints
|
||||
)
|
||||
revision = int(data["revision"])
|
||||
_rebuild_pixels()
|
||||
if is_hidden_by_relationship():
|
||||
_rebuild_pixel_image()
|
||||
else:
|
||||
_apply_pixel_edits(data["edits"])
|
||||
_refresh_grid_visibility()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -96,15 +107,32 @@ func apply_guide_update(data: Dictionary) -> bool:
|
|||
):
|
||||
return false
|
||||
revision = int(data["revision"])
|
||||
participant_fingerprints = _participants_from_update(
|
||||
data, participant_fingerprints
|
||||
)
|
||||
_finalized = bool(data["finalized"])
|
||||
_guide_visible = bool(data["guide_visible"]) and not _finalized
|
||||
_refresh_grid_visibility()
|
||||
_rebuild_pixels()
|
||||
if _finalized:
|
||||
_destroy_grid()
|
||||
else:
|
||||
_refresh_grid_visibility()
|
||||
return true
|
||||
|
||||
|
||||
func refresh_relationship_visibility() -> void:
|
||||
_rebuild_pixels()
|
||||
_rebuild_pixel_image()
|
||||
if _pixel_instance != null:
|
||||
_pixel_instance.visible = not is_hidden_by_relationship()
|
||||
_refresh_grid_visibility()
|
||||
|
||||
|
||||
func is_hidden_by_relationship() -> bool:
|
||||
if _relationships == null:
|
||||
return false
|
||||
for fingerprint: String in participant_fingerprints:
|
||||
if _relationships.is_blocked(fingerprint):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func set_stencil_visible(should_be_visible: bool) -> void:
|
||||
|
|
@ -121,7 +149,27 @@ func is_finalized() -> bool:
|
|||
|
||||
|
||||
func get_rendered_pixel_size() -> float:
|
||||
return cell_size * _pixel_fill()
|
||||
return cell_size
|
||||
|
||||
|
||||
func get_rendered_pixel_count() -> int:
|
||||
if is_hidden_by_relationship():
|
||||
return 0
|
||||
var count: int = 0
|
||||
for cell: Dictionary in _cells.values():
|
||||
if not _visible_cell_color(cell).is_equal_approx(Color.TRANSPARENT):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func get_export_image() -> Image:
|
||||
if _pixel_image == null or _pixel_image.is_empty():
|
||||
return null
|
||||
return _pixel_image.duplicate() as Image
|
||||
|
||||
|
||||
func has_guide_geometry() -> bool:
|
||||
return _grid_instance != null and is_instance_valid(_grid_instance)
|
||||
|
||||
|
||||
func contains_world_point(world_point: Vector3, tolerance: float = 0.3) -> bool:
|
||||
|
|
@ -192,7 +240,18 @@ func get_cell_surface_transform(
|
|||
) -> Transform3D:
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return Transform3D.IDENTITY
|
||||
return global_transform * _sample_cell_transform(x, y, fill)
|
||||
var key: int = _cell_key(x, y)
|
||||
var local_transform: Transform3D = (
|
||||
_cell_surface_transforms[key]
|
||||
if key >= 0 and key < _cell_surface_transforms.size()
|
||||
else _sample_cell_transform(x, y, 1.0)
|
||||
)
|
||||
var scaled_basis: Basis = local_transform.basis
|
||||
var resolved_fill: float = clampf(fill, 0.05, 1.0)
|
||||
scaled_basis.x *= resolved_fill
|
||||
scaled_basis.y *= resolved_fill
|
||||
local_transform.basis = scaled_basis
|
||||
return global_transform * local_transform
|
||||
|
||||
|
||||
func get_authoritative_cells() -> Array[Dictionary]:
|
||||
|
|
@ -210,6 +269,9 @@ func get_authoritative_cells() -> Array[Dictionary]:
|
|||
func _build_grid() -> void:
|
||||
if _grid_instance != null:
|
||||
_grid_instance.queue_free()
|
||||
_grid_instance = null
|
||||
if _finalized:
|
||||
return
|
||||
var immediate := ImmediateMesh.new()
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
|
|
@ -219,6 +281,7 @@ func _build_grid() -> void:
|
|||
immediate.surface_begin(Mesh.PRIMITIVE_LINES, material)
|
||||
var half_width: float = float(grid_width) * cell_size * 0.5
|
||||
var half_height: float = float(grid_height) * cell_size * 0.5
|
||||
var sampled_vertices: Dictionary[Vector2i, Dictionary] = {}
|
||||
for x: int in range(grid_width + 1):
|
||||
var horizontal: float = -half_width + float(x) * cell_size
|
||||
for y_segment: int in range(grid_height):
|
||||
|
|
@ -229,11 +292,15 @@ func _build_grid() -> void:
|
|||
immediate,
|
||||
_surface_origin + _surface_tangent * horizontal
|
||||
+ _surface_bitangent * start_vertical,
|
||||
Vector2i(x, y_segment),
|
||||
sampled_vertices,
|
||||
)
|
||||
_add_grid_vertex(
|
||||
immediate,
|
||||
_surface_origin + _surface_tangent * horizontal
|
||||
+ _surface_bitangent * (start_vertical + cell_size),
|
||||
Vector2i(x, y_segment + 1),
|
||||
sampled_vertices,
|
||||
)
|
||||
for y: int in range(grid_height + 1):
|
||||
var vertical: float = -half_height + float(y) * cell_size
|
||||
|
|
@ -245,12 +312,16 @@ func _build_grid() -> void:
|
|||
immediate,
|
||||
_surface_origin + _surface_tangent * start_horizontal
|
||||
+ _surface_bitangent * vertical,
|
||||
Vector2i(x_segment, y),
|
||||
sampled_vertices,
|
||||
)
|
||||
_add_grid_vertex(
|
||||
immediate,
|
||||
_surface_origin
|
||||
+ _surface_tangent * (start_horizontal + cell_size)
|
||||
+ _surface_bitangent * vertical,
|
||||
Vector2i(x_segment + 1, y),
|
||||
sampled_vertices,
|
||||
)
|
||||
immediate.surface_end()
|
||||
_grid_instance = MeshInstance3D.new()
|
||||
|
|
@ -261,8 +332,16 @@ func _build_grid() -> void:
|
|||
_refresh_grid_visibility()
|
||||
|
||||
|
||||
func _add_grid_vertex(immediate: ImmediateMesh, world_point: Vector3) -> void:
|
||||
var sampled: Dictionary = _sample_surface(world_point)
|
||||
func _add_grid_vertex(
|
||||
immediate: ImmediateMesh,
|
||||
world_point: Vector3,
|
||||
grid_point: Vector2i,
|
||||
sampled_vertices: Dictionary[Vector2i, Dictionary],
|
||||
) -> void:
|
||||
var sampled: Dictionary = sampled_vertices.get(grid_point, {})
|
||||
if sampled.is_empty():
|
||||
sampled = _sample_surface(world_point)
|
||||
sampled_vertices[grid_point] = sampled
|
||||
var point: Vector3 = sampled["position"]
|
||||
var normal: Vector3 = sampled["normal"]
|
||||
immediate.surface_add_vertex(
|
||||
|
|
@ -270,56 +349,130 @@ func _add_grid_vertex(immediate: ImmediateMesh, world_point: Vector3) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _rebuild_pixels() -> void:
|
||||
for instance: MultiMeshInstance3D in _color_meshes.values():
|
||||
instance.queue_free()
|
||||
_color_meshes.clear()
|
||||
var cells_by_color: Dictionary[StringName, Array] = {}
|
||||
for cell: Dictionary in _cells.values():
|
||||
var author_fingerprint: String = str(cell.get("author_fingerprint", ""))
|
||||
if (
|
||||
_relationships != null
|
||||
and _relationships.is_blocked(author_fingerprint)
|
||||
):
|
||||
continue
|
||||
var color_id := StringName(str(cell.get("color_id", "")))
|
||||
if not SurfaceDrawingPalette.has_color(color_id):
|
||||
continue
|
||||
var color_cells: Array = cells_by_color.get(color_id, [])
|
||||
color_cells.append(cell)
|
||||
cells_by_color[color_id] = color_cells
|
||||
for color_id: StringName in cells_by_color:
|
||||
_create_color_mesh(color_id, cells_by_color[color_id])
|
||||
|
||||
|
||||
func _create_color_mesh(color_id: StringName, cells: Array) -> void:
|
||||
if cells.is_empty():
|
||||
return
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2.ONE
|
||||
func _build_pixel_renderer() -> void:
|
||||
if _pixel_instance != null:
|
||||
_pixel_instance.queue_free()
|
||||
_pixel_instance = null
|
||||
var cell_count: int = grid_width * grid_height
|
||||
var vertices := PackedVector3Array()
|
||||
var texture_coordinates := PackedVector2Array()
|
||||
var indices := PackedInt32Array()
|
||||
vertices.resize(cell_count * 4)
|
||||
texture_coordinates.resize(cell_count * 4)
|
||||
indices.resize(cell_count * 6)
|
||||
_cell_surface_transforms.clear()
|
||||
_cell_surface_transforms.resize(cell_count)
|
||||
for y: int in range(grid_height):
|
||||
for x: int in range(grid_width):
|
||||
var cell_index: int = _cell_key(x, y)
|
||||
var surface_transform := _sample_cell_transform(x, y, 1.0)
|
||||
_cell_surface_transforms[cell_index] = surface_transform
|
||||
var center: Vector3 = surface_transform.origin
|
||||
var horizontal: Vector3 = surface_transform.basis.x * 0.5
|
||||
var vertical: Vector3 = surface_transform.basis.y * 0.5
|
||||
var vertex_offset: int = cell_index * 4
|
||||
vertices[vertex_offset] = center - horizontal - vertical
|
||||
vertices[vertex_offset + 1] = center + horizontal - vertical
|
||||
vertices[vertex_offset + 2] = center + horizontal + vertical
|
||||
vertices[vertex_offset + 3] = center - horizontal + vertical
|
||||
var image_row: int = _image_y(y)
|
||||
var u_min: float = float(x) / float(grid_width)
|
||||
var u_max: float = float(x + 1) / float(grid_width)
|
||||
var v_min: float = float(image_row) / float(grid_height)
|
||||
var v_max: float = float(image_row + 1) / float(grid_height)
|
||||
texture_coordinates[vertex_offset] = Vector2(u_min, v_max)
|
||||
texture_coordinates[vertex_offset + 1] = Vector2(u_max, v_max)
|
||||
texture_coordinates[vertex_offset + 2] = Vector2(u_max, v_min)
|
||||
texture_coordinates[vertex_offset + 3] = Vector2(u_min, v_min)
|
||||
var index_offset: int = cell_index * 6
|
||||
indices[index_offset] = vertex_offset
|
||||
indices[index_offset + 1] = vertex_offset + 1
|
||||
indices[index_offset + 2] = vertex_offset + 2
|
||||
indices[index_offset + 3] = vertex_offset
|
||||
indices[index_offset + 4] = vertex_offset + 2
|
||||
indices[index_offset + 5] = vertex_offset + 3
|
||||
_rebuild_pixel_image()
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_TEX_UV] = texture_coordinates
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = SurfaceDrawingPalette.get_color(color_id)
|
||||
material.albedo_color = Color.WHITE
|
||||
material.albedo_texture = _pixel_texture
|
||||
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
material.alpha_scissor_threshold = 0.5
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
quad.material = material
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.mesh = quad
|
||||
multimesh.instance_count = cells.size()
|
||||
for cell_index: int in range(cells.size()):
|
||||
var cell: Dictionary = cells[cell_index]
|
||||
multimesh.set_instance_transform(
|
||||
cell_index,
|
||||
_sample_cell_transform(
|
||||
int(cell["x"]), int(cell["y"]), _pixel_fill()
|
||||
),
|
||||
mesh.surface_set_material(0, material)
|
||||
_pixel_instance = MeshInstance3D.new()
|
||||
_pixel_instance.name = "ArtworkTexture"
|
||||
_pixel_instance.mesh = mesh
|
||||
_pixel_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(_pixel_instance)
|
||||
_pixel_instance.visible = not is_hidden_by_relationship()
|
||||
|
||||
|
||||
func _rebuild_pixel_image() -> void:
|
||||
if (
|
||||
_pixel_image == null
|
||||
or _pixel_image.get_width() != grid_width
|
||||
or _pixel_image.get_height() != grid_height
|
||||
):
|
||||
_pixel_image = Image.create(
|
||||
grid_width, grid_height, false, Image.FORMAT_RGBA8
|
||||
)
|
||||
var instance := MultiMeshInstance3D.new()
|
||||
instance.name = "Pixels_%s" % color_id
|
||||
instance.multimesh = multimesh
|
||||
instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(instance)
|
||||
_color_meshes[color_id] = instance
|
||||
_pixel_image.fill(Color.TRANSPARENT)
|
||||
if not is_hidden_by_relationship():
|
||||
for cell: Dictionary in _cells.values():
|
||||
_set_image_cell(int(cell["x"]), int(cell["y"]), cell)
|
||||
if _pixel_texture == null:
|
||||
_pixel_texture = ImageTexture.create_from_image(_pixel_image)
|
||||
else:
|
||||
_pixel_texture.update(_pixel_image)
|
||||
|
||||
|
||||
func _apply_pixel_edits(edits: Array) -> void:
|
||||
if _pixel_image == null or _pixel_texture == null:
|
||||
_rebuild_pixel_image()
|
||||
return
|
||||
for edit_value: Variant in edits:
|
||||
var edit: Dictionary = edit_value
|
||||
var x: int = int(edit["x"])
|
||||
var y: int = int(edit["y"])
|
||||
var cell: Dictionary = _cells.get(_cell_key(x, y), {})
|
||||
_set_image_cell(x, y, cell)
|
||||
_pixel_texture.update(_pixel_image)
|
||||
|
||||
|
||||
func _set_image_cell(x: int, y: int, cell: Dictionary) -> void:
|
||||
if (
|
||||
_pixel_image == null
|
||||
or x < 0
|
||||
or x >= grid_width
|
||||
or y < 0
|
||||
or y >= grid_height
|
||||
):
|
||||
return
|
||||
_pixel_image.set_pixel(x, _image_y(y), _visible_cell_color(cell))
|
||||
|
||||
|
||||
func _visible_cell_color(cell: Dictionary) -> Color:
|
||||
if cell.is_empty():
|
||||
return Color.TRANSPARENT
|
||||
var color_id := StringName(str(cell.get("color_id", "")))
|
||||
return (
|
||||
SurfaceDrawingPalette.get_color(color_id)
|
||||
if SurfaceDrawingPalette.has_color(color_id)
|
||||
else Color.TRANSPARENT
|
||||
)
|
||||
|
||||
|
||||
func _image_y(cell_y: int) -> int:
|
||||
return grid_height - 1 - cell_y
|
||||
|
||||
|
||||
func _sample_cell_transform(
|
||||
|
|
@ -376,17 +529,60 @@ func _sample_surface(expected: Vector3) -> Dictionary:
|
|||
return {"position": point, "normal": normal}
|
||||
|
||||
|
||||
func _pixel_fill() -> float:
|
||||
return GUIDED_PIXEL_FILL if _guide_visible else FINISHED_PIXEL_FILL
|
||||
|
||||
|
||||
func _surface_offset() -> float:
|
||||
return SURFACE_OFFSET + float(_layer) * LAYER_OFFSET_STEP
|
||||
|
||||
|
||||
func _destroy_grid() -> void:
|
||||
if _grid_instance == null:
|
||||
return
|
||||
_grid_instance.queue_free()
|
||||
_grid_instance = null
|
||||
|
||||
|
||||
func _refresh_grid_visibility() -> void:
|
||||
if _grid_instance != null:
|
||||
_grid_instance.visible = _stencil_requested_visible and _guide_visible
|
||||
_grid_instance.visible = (
|
||||
_stencil_requested_visible
|
||||
and _guide_visible
|
||||
and not is_hidden_by_relationship()
|
||||
)
|
||||
|
||||
|
||||
func _participants_from_state(data: Dictionary) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
_append_participant(result, str(data.get("creator_fingerprint", "")))
|
||||
for value: Variant in data.get("participant_fingerprints", []):
|
||||
_append_participant(result, str(value))
|
||||
for cell_value: Variant in data.get("cells", []):
|
||||
var cell: Dictionary = cell_value
|
||||
_append_participant(
|
||||
result, str(cell.get("author_fingerprint", ""))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func _participants_from_update(
|
||||
data: Dictionary,
|
||||
fallback: Array[String],
|
||||
) -> Array[String]:
|
||||
var result: Array[String] = fallback.duplicate()
|
||||
for value: Variant in data.get("participant_fingerprints", []):
|
||||
_append_participant(result, str(value))
|
||||
for edit_value: Variant in data.get("edits", []):
|
||||
var edit: Dictionary = edit_value
|
||||
_append_participant(
|
||||
result, str(edit.get("author_fingerprint", ""))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func _append_participant(result: Array[String], fingerprint: String) -> void:
|
||||
if (
|
||||
NetworkIdentityCrypto.valid_fingerprint(fingerprint)
|
||||
and fingerprint not in result
|
||||
):
|
||||
result.append(fingerprint)
|
||||
|
||||
|
||||
func _cell_key(x: int, y: int) -> int:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ const MAX_CANVAS_ID_LENGTH: int = 64
|
|||
const MAX_REQUEST_ID_LENGTH: int = 64
|
||||
const MAX_STROKE_ID_LENGTH: int = 64
|
||||
const MAX_SESSION_ID_LENGTH: int = 96
|
||||
# Sessions support up to 128 simultaneous players. Keep enough bounded history
|
||||
# for a complete room turnover while never accepting unattributed edits.
|
||||
const MAX_PARTICIPANTS: int = 256
|
||||
const MAX_COORDINATE: float = 10000.0
|
||||
|
||||
|
||||
|
|
@ -64,6 +67,38 @@ static func validate_edit_request(data: Variant) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
static func validate_stamp_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
if (
|
||||
not _valid_common(value)
|
||||
or not _valid_vector(value.get("origin"))
|
||||
or not _valid_vector(value.get("normal"))
|
||||
or not _valid_vector(value.get("tangent"))
|
||||
or typeof(value.get("width")) != TYPE_INT
|
||||
or int(value["width"]) not in GRID_SIZES
|
||||
or typeof(value.get("height")) != TYPE_INT
|
||||
or int(value["height"]) != int(value["width"])
|
||||
or typeof(value.get("cell_size")) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or not is_equal_approx(float(value["cell_size"]), CELL_SIZE)
|
||||
or typeof(value.get("pixels")) != TYPE_PACKED_BYTE_ARRAY
|
||||
):
|
||||
return false
|
||||
var pixels: PackedByteArray = value["pixels"]
|
||||
var expected_size: int = int(value["width"]) * int(value["height"])
|
||||
if pixels.size() != expected_size:
|
||||
return false
|
||||
var maximum_palette_index: int = SurfaceDrawingPalette.COLORS.size()
|
||||
var painted_count: int = 0
|
||||
for palette_index: int in pixels:
|
||||
if palette_index > maximum_palette_index:
|
||||
return false
|
||||
if palette_index > 0:
|
||||
painted_count += 1
|
||||
return painted_count > 0
|
||||
|
||||
|
||||
static func validate_guide_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
|
|
@ -112,6 +147,9 @@ static func validate_canvas_state(data: Variant) -> bool:
|
|||
or not NetworkIdentityCrypto.valid_fingerprint(
|
||||
value.get("creator_fingerprint")
|
||||
)
|
||||
or not _valid_participant_fingerprints(
|
||||
value.get("participant_fingerprints", [])
|
||||
)
|
||||
or typeof(value.get("cells")) != TYPE_ARRAY
|
||||
):
|
||||
return false
|
||||
|
|
@ -127,6 +165,13 @@ static func validate_canvas_state(data: Variant) -> bool:
|
|||
or int((cell_value as Dictionary)["y"]) >= grid_height
|
||||
):
|
||||
return false
|
||||
var participants: Array = value.get("participant_fingerprints", [])
|
||||
if not participants.is_empty():
|
||||
if str(value["creator_fingerprint"]) not in participants:
|
||||
return false
|
||||
for cell_value: Variant in cells:
|
||||
if str((cell_value as Dictionary)["author_fingerprint"]) not in participants:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -143,6 +188,9 @@ static func validate_guide_update(data: Variant) -> bool:
|
|||
and int(value["revision"]) >= 1
|
||||
and typeof(value.get("guide_visible")) == TYPE_BOOL
|
||||
and typeof(value.get("finalized")) == TYPE_BOOL
|
||||
and _valid_participant_fingerprints(
|
||||
value.get("participant_fingerprints", [])
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -160,6 +208,9 @@ static func validate_canvas_update(data: Variant) -> bool:
|
|||
or typeof(value.get("revision")) != TYPE_INT
|
||||
or int(value["revision"]) < 1
|
||||
or typeof(value.get("edits")) != TYPE_ARRAY
|
||||
or not _valid_participant_fingerprints(
|
||||
value.get("participant_fingerprints", [])
|
||||
)
|
||||
):
|
||||
return false
|
||||
var edits: Array = value["edits"]
|
||||
|
|
@ -168,6 +219,18 @@ static func validate_canvas_update(data: Variant) -> bool:
|
|||
for edit_value: Variant in edits:
|
||||
if not validate_authoritative_cell(edit_value, true):
|
||||
return false
|
||||
var participants: Array = value.get("participant_fingerprints", [])
|
||||
if not participants.is_empty():
|
||||
for edit_value: Variant in edits:
|
||||
var edit: Dictionary = edit_value
|
||||
var author_fingerprint: String = str(
|
||||
edit.get("author_fingerprint", "")
|
||||
)
|
||||
if (
|
||||
not str(edit.get("color_id", "")).is_empty()
|
||||
and author_fingerprint not in participants
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -242,6 +305,26 @@ static func _valid_stroke_id(value: Variant) -> bool:
|
|||
)
|
||||
|
||||
|
||||
static func _valid_participant_fingerprints(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return false
|
||||
var fingerprints: Array = value
|
||||
if fingerprints.size() > MAX_PARTICIPANTS:
|
||||
return false
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for fingerprint_value: Variant in fingerprints:
|
||||
if typeof(fingerprint_value) != TYPE_STRING:
|
||||
return false
|
||||
var fingerprint: String = str(fingerprint_value)
|
||||
if (
|
||||
not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
|
||||
or seen.has(fingerprint)
|
||||
):
|
||||
return false
|
||||
seen[fingerprint] = true
|
||||
return true
|
||||
|
||||
|
||||
static func _valid_vector(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue