Optimize finalized surface artwork

This commit is contained in:
Alexander Sellite 2026-09-01 13:05:05 -04:00
parent 82938d3d57
commit f2239d248a
14 changed files with 1254 additions and 305 deletions

View file

@ -5,6 +5,9 @@ const GUIDED_PIXEL_FILL: float = 0.88
const SURFACE_OFFSET: float = 0.012
const LAYER_OFFSET_STEP: float = 0.0002
const SURFACE_SAMPLE_DEPTH: float = 0.45
const FINALIZED_VISIBILITY_RANGE: float = 96.0
const FINALIZED_VISIBILITY_MARGIN: float = 12.0
const FINALIZED_MAX_SUBDIVISIONS: int = 32
var canvas_id: String = ""
var grid_width: int = 0
@ -18,7 +21,7 @@ var _surface_origin: Vector3
var _surface_normal: Vector3
var _surface_tangent: Vector3
var _surface_bitangent: Vector3
var _cells: Dictionary[int, Dictionary] = {}
var _cell_buffer: SurfaceDrawingCellBuffer
var _cell_surface_transforms: Array[Transform3D] = []
var _pixel_image: Image
var _pixel_texture: ImageTexture
@ -36,8 +39,15 @@ func setup(
data: Dictionary,
relationships: PlayerRelationshipStore,
solid_surface_mask: int = 1,
cell_buffer: SurfaceDrawingCellBuffer = null,
) -> bool:
if not SurfaceDrawingProtocol.validate_canvas_state(data):
if (
(cell_buffer == null and not SurfaceDrawingProtocol.validate_canvas_state(data))
or (
cell_buffer != null
and not SurfaceDrawingProtocol.validate_canvas_metadata(data)
)
):
return false
canvas_id = str(data["canvas_id"])
grid_width = int(data["width"])
@ -45,7 +55,6 @@ 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))
@ -64,12 +73,20 @@ func setup(
_surface_bitangent = _surface_normal.cross(_surface_tangent).normalized()
_relationships = relationships
_solid_surface_mask = solid_surface_mask
_cells.clear()
for cell_value: Variant in data["cells"]:
var cell: Dictionary = cell_value
_cells[_cell_key(int(cell["x"]), int(cell["y"]))] = cell.duplicate(true)
_build_grid()
_build_pixel_renderer()
_cell_buffer = cell_buffer
if _cell_buffer == null:
_cell_buffer = SurfaceDrawingCellBuffer.new()
if not _cell_buffer.configure_from_state(data, true):
return false
elif _cell_buffer.width != grid_width or _cell_buffer.height != grid_height:
return false
participant_fingerprints = _cell_buffer.get_participants()
if _finalized:
_build_finalized_pixel_renderer()
else:
var samples: Dictionary = _sample_grid_vertices()
_build_grid(samples)
_build_pixel_renderer(samples)
return true
@ -80,16 +97,25 @@ func apply_update(data: Dictionary) -> bool:
or int(data["revision"]) <= revision
):
return false
for edit_value: Variant in data["edits"]:
var edit: Dictionary = edit_value
var key: int = _cell_key(int(edit["x"]), int(edit["y"]))
if str(edit["color_id"]).is_empty():
_cells.erase(key)
else:
_cells[key] = edit.duplicate(true)
participant_fingerprints = _participants_from_update(
data, participant_fingerprints
)
if _cell_buffer == null or not _cell_buffer.apply_edits(data["edits"]):
return false
return _finish_buffered_update(data)
func apply_buffered_update(data: Dictionary) -> bool:
if (
not SurfaceDrawingProtocol.validate_canvas_update(data)
or str(data["canvas_id"]) != canvas_id
or int(data["revision"]) <= revision
or _cell_buffer == null
):
return false
return _finish_buffered_update(data)
func _finish_buffered_update(data: Dictionary) -> bool:
_cell_buffer.ensure_participants(data.get("participant_fingerprints", []))
participant_fingerprints = _cell_buffer.get_participants()
revision = int(data["revision"])
if is_hidden_by_relationship():
_rebuild_pixel_image()
@ -110,12 +136,18 @@ func apply_guide_update(data: Dictionary) -> bool:
participant_fingerprints = _participants_from_update(
data, participant_fingerprints
)
if _cell_buffer != null:
_cell_buffer.ensure_participants(participant_fingerprints)
participant_fingerprints = _cell_buffer.get_participants()
_finalized = bool(data["finalized"])
_guide_visible = bool(data["guide_visible"]) and not _finalized
if _finalized:
_destroy_grid()
_cell_surface_transforms.clear()
_build_finalized_pixel_renderer()
else:
_refresh_grid_visibility()
_configure_render_distance()
return true
@ -155,11 +187,7 @@ func get_rendered_pixel_size() -> float:
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
return _cell_buffer.get_painted_count() if _cell_buffer != null else 0
func get_export_image() -> Image:
@ -255,18 +283,10 @@ func get_cell_surface_transform(
func get_authoritative_cells() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for value: Dictionary in _cells.values():
result.append(value.duplicate(true))
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var a_key: int = _cell_key(int(a["x"]), int(a["y"]))
var b_key: int = _cell_key(int(b["x"]), int(b["y"]))
return a_key < b_key
)
return result
return _cell_buffer.to_cells() if _cell_buffer != null else []
func _build_grid() -> void:
func _build_grid(samples: Dictionary) -> void:
if _grid_instance != null:
_grid_instance.queue_free()
_grid_instance = null
@ -279,50 +299,14 @@ func _build_grid() -> void:
material.albedo_color = Color(0.75, 0.94, 0.96, 0.38)
material.no_depth_test = false
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):
var start_vertical: float = (
-half_height + float(y_segment) * cell_size
)
_add_grid_vertex(
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,
)
_add_grid_vertex(immediate, x, y_segment, samples)
_add_grid_vertex(immediate, x, y_segment + 1, samples)
for y: int in range(grid_height + 1):
var vertical: float = -half_height + float(y) * cell_size
for x_segment: int in range(grid_width):
var start_horizontal: float = (
-half_width + float(x_segment) * cell_size
)
_add_grid_vertex(
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,
)
_add_grid_vertex(immediate, x_segment, y, samples)
_add_grid_vertex(immediate, x_segment + 1, y, samples)
immediate.surface_end()
_grid_instance = MeshInstance3D.new()
_grid_instance.name = "PixelGrid"
@ -334,63 +318,56 @@ func _build_grid() -> void:
func _add_grid_vertex(
immediate: ImmediateMesh,
world_point: Vector3,
grid_point: Vector2i,
sampled_vertices: Dictionary[Vector2i, Dictionary],
x: int,
y: int,
samples: 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(
to_local(point + normal * (_surface_offset() * 1.5))
_sample_position(samples, x, y, _surface_offset() * 1.5)
)
func _build_pixel_renderer() -> void:
func _build_pixel_renderer(samples: Dictionary) -> void:
if _pixel_instance != null:
_pixel_instance.queue_free()
_pixel_instance = null
var vertex_width: int = grid_width + 1
var vertex_count: int = vertex_width * (grid_height + 1)
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)
vertices.resize(vertex_count)
texture_coordinates.resize(vertex_count)
indices.resize(cell_count * 6)
_cell_surface_transforms.clear()
_cell_surface_transforms.resize(cell_count)
if not _finalized:
_cell_surface_transforms.resize(cell_count)
for y: int in range(grid_height + 1):
for x: int in range(grid_width + 1):
var vertex_index: int = y * vertex_width + x
vertices[vertex_index] = _sample_position(
samples, x, y, _surface_offset()
)
texture_coordinates[vertex_index] = Vector2(
float(x) / float(grid_width),
1.0 - float(y) / float(grid_height),
)
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)
if not _finalized:
_cell_surface_transforms[cell_index] = _transform_from_samples(
samples, x, y
)
var vertex_offset: int = y * vertex_width + x
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 + 2] = vertex_offset + vertex_width + 1
indices[index_offset + 3] = vertex_offset
indices[index_offset + 4] = vertex_offset + 2
indices[index_offset + 5] = vertex_offset + 3
indices[index_offset + 4] = vertex_offset + vertex_width + 1
indices[index_offset + 5] = vertex_offset + vertex_width
_rebuild_pixel_image()
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
@ -414,21 +391,103 @@ func _build_pixel_renderer() -> void:
_pixel_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
add_child(_pixel_instance)
_pixel_instance.visible = not is_hidden_by_relationship()
_configure_render_distance()
func _build_finalized_pixel_renderer() -> void:
if _pixel_instance != null:
_pixel_instance.queue_free()
_pixel_instance = null
var step: int = maxi(
1,
ceili(float(grid_width) / float(FINALIZED_MAX_SUBDIVISIONS)),
)
var coordinates := PackedInt32Array()
var coordinate: int = 0
while coordinate < grid_width:
coordinates.append(coordinate)
coordinate += step
if coordinates.is_empty() or coordinates[-1] != grid_width:
coordinates.append(grid_width)
var vertex_width: int = coordinates.size()
var vertices := PackedVector3Array()
var texture_coordinates := PackedVector2Array()
var indices := PackedInt32Array()
vertices.resize(vertex_width * vertex_width)
texture_coordinates.resize(vertex_width * vertex_width)
indices.resize((vertex_width - 1) * (vertex_width - 1) * 6)
var half_width: float = float(grid_width) * cell_size * 0.5
var half_height: float = float(grid_height) * cell_size * 0.5
for lattice_y: int in vertex_width:
var cell_y: int = coordinates[lattice_y]
var vertical: float = -half_height + float(cell_y) * cell_size
for lattice_x: int in vertex_width:
var cell_x: int = coordinates[lattice_x]
var horizontal: float = -half_width + float(cell_x) * cell_size
var expected: Vector3 = (
_surface_origin
+ _surface_tangent * horizontal
+ _surface_bitangent * vertical
)
var sampled: Dictionary = _sample_surface(expected)
var vertex_index: int = lattice_y * vertex_width + lattice_x
var point: Vector3 = sampled["position"]
var normal: Vector3 = sampled["normal"]
vertices[vertex_index] = to_local(
point + normal * _surface_offset()
)
texture_coordinates[vertex_index] = Vector2(
float(cell_x) / float(grid_width),
1.0 - float(cell_y) / float(grid_height),
)
for lattice_y: int in range(vertex_width - 1):
for lattice_x: int in range(vertex_width - 1):
var cell_index: int = lattice_y * (vertex_width - 1) + lattice_x
var vertex_offset: int = lattice_y * vertex_width + lattice_x
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 + vertex_width + 1
indices[index_offset + 3] = vertex_offset
indices[index_offset + 4] = vertex_offset + vertex_width + 1
indices[index_offset + 5] = vertex_offset + vertex_width
if _pixel_image == null or _pixel_texture == null:
_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 = 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
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()
_configure_render_distance()
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
)
_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 _cell_buffer == null:
return
_pixel_image = Image.create_from_data(
grid_width,
grid_height,
false,
Image.FORMAT_RGBA8,
_cell_buffer.to_rgba8_data(is_hidden_by_relationship()),
)
if _pixel_texture == null:
_pixel_texture = ImageTexture.create_from_image(_pixel_image)
else:
@ -443,7 +502,7 @@ func _apply_pixel_edits(edits: Array) -> void:
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), {})
var cell: Dictionary = _cell_buffer.get_cell(x, y)
_set_image_cell(x, y, cell)
_pixel_texture.update(_pixel_image)
@ -475,6 +534,78 @@ func _image_y(cell_y: int) -> int:
return grid_height - 1 - cell_y
func _sample_grid_vertices() -> Dictionary:
var vertex_width: int = grid_width + 1
var vertex_count: int = vertex_width * (grid_height + 1)
var positions := PackedVector3Array()
var normals := PackedVector3Array()
positions.resize(vertex_count)
normals.resize(vertex_count)
var half_width: float = float(grid_width) * cell_size * 0.5
var half_height: float = float(grid_height) * cell_size * 0.5
var inverse_basis: Basis = global_basis.inverse()
for y: int in range(grid_height + 1):
var vertical: float = -half_height + float(y) * cell_size
for x: int in range(grid_width + 1):
var horizontal: float = -half_width + float(x) * cell_size
var expected: Vector3 = (
_surface_origin
+ _surface_tangent * horizontal
+ _surface_bitangent * vertical
)
var sampled: Dictionary = _sample_surface(expected)
var index: int = y * vertex_width + x
positions[index] = to_local(sampled["position"])
normals[index] = (
inverse_basis * (sampled["normal"] as Vector3)
).normalized()
return {"positions": positions, "normals": normals}
func _sample_position(
samples: Dictionary,
x: int,
y: int,
offset: float,
) -> Vector3:
var index: int = y * (grid_width + 1) + x
var positions: PackedVector3Array = samples["positions"]
var normals: PackedVector3Array = samples["normals"]
return positions[index] + normals[index] * offset
func _transform_from_samples(
samples: Dictionary,
x: int,
y: int,
) -> Transform3D:
var bottom_left: Vector3 = _sample_position(
samples, x, y, _surface_offset()
)
var bottom_right: Vector3 = _sample_position(
samples, x + 1, y, _surface_offset()
)
var top_left: Vector3 = _sample_position(
samples, x, y + 1, _surface_offset()
)
var top_right: Vector3 = _sample_position(
samples, x + 1, y + 1, _surface_offset()
)
var horizontal: Vector3 = (
(bottom_right - bottom_left) + (top_right - top_left)
) * 0.5
var vertical: Vector3 = (
(top_left - bottom_left) + (top_right - bottom_right)
) * 0.5
var normal: Vector3 = horizontal.cross(vertical).normalized()
if horizontal.is_zero_approx() or vertical.is_zero_approx() or normal.is_zero_approx():
return _sample_cell_transform(x, y, 1.0)
var center: Vector3 = (
bottom_left + bottom_right + top_left + top_right
) * 0.25
return Transform3D(Basis(horizontal, vertical, normal), center)
func _sample_cell_transform(
x: int,
y: int,
@ -549,6 +680,17 @@ func _refresh_grid_visibility() -> void:
)
func _configure_render_distance() -> void:
if _pixel_instance == null:
return
_pixel_instance.visibility_range_end = (
FINALIZED_VISIBILITY_RANGE if _finalized else 0.0
)
_pixel_instance.visibility_range_end_margin = (
FINALIZED_VISIBILITY_MARGIN if _finalized else 0.0
)
func _participants_from_state(data: Dictionary) -> Array[String]:
var result: Array[String] = []
_append_participant(result, str(data.get("creator_fingerprint", "")))

View file

@ -0,0 +1,284 @@
# 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

View file

@ -0,0 +1 @@
uid://b4clw478edbnk

View file

@ -45,6 +45,31 @@ static func get_color_ids() -> Array[StringName]:
return result
static func palette_index_for_id(color_id: StringName) -> int:
for index: int in COLORS.size():
if StringName(COLORS[index].get("id", &"")) == color_id:
return index + 1
return 0
static func id_for_palette_index(palette_index: int) -> StringName:
var index: int = palette_index - 1
return (
StringName(COLORS[index].get("id", &""))
if index >= 0 and index < COLORS.size()
else StringName()
)
static func color_for_palette_index(palette_index: int) -> Color:
var index: int = palette_index - 1
return (
COLORS[index].get("color", Color.TRANSPARENT)
if index >= 0 and index < COLORS.size()
else Color.TRANSPARENT
)
static func filter_unlocked_ids(
unlocked_ids: Array[StringName],
) -> Array[StringName]:

View file

@ -25,7 +25,7 @@ static func resolve(
}
var nearest_distance: float = SNAP_DISTANCE
for state: Dictionary in canvas_states:
if not SurfaceDrawingProtocol.validate_canvas_state(state):
if not SurfaceDrawingProtocol.validate_canvas_metadata(state):
continue
var anchor_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
state["normal"]

View file

@ -1,7 +1,7 @@
class_name SurfaceDrawingProtocol
extends RefCounted
const CAPABILITY: StringName = &"surface_drawing_v2"
const CAPABILITY: StringName = &"surface_drawing_v3"
const RELIABLE_CHANNEL: int = NetworkProtocol.DRAWING_RELIABLE_CHANNEL
const GRID_SIZES: Array[int] = [16, 32, 64, 128]
const DEFAULT_GRID_SIZE: int = 16
@ -11,8 +11,12 @@ const GRID_WIDTH: int = DEFAULT_GRID_SIZE
const GRID_HEIGHT: int = DEFAULT_GRID_SIZE
const CELL_SIZE: float = 0.075
const MAX_ACTIVE_CANVASES: int = 24
const MAX_CANVASES: int = 48
const MAX_SESSION_GRID_CELLS: int = 49152
const MAX_FINALIZED_CANVASES: int = 512
const MAX_CANVASES: int = MAX_ACTIVE_CANVASES + MAX_FINALIZED_CANVASES
const MAX_ACTIVE_GRID_CELLS: int = 49152
const MAX_FINALIZED_GRID_CELLS: int = 8 * 1024 * 1024
# Compatibility alias for code concerned only with live editing capacity.
const MAX_SESSION_GRID_CELLS: int = MAX_ACTIVE_GRID_CELLS
const MAX_EDITS_PER_REQUEST: int = 16
const MAX_CANVAS_ID_LENGTH: int = 64
const MAX_REQUEST_ID_LENGTH: int = 64
@ -22,6 +26,7 @@ const MAX_SESSION_ID_LENGTH: int = 96
# for a complete room turnover while never accepting unattributed edits.
const MAX_PARTICIPANTS: int = 256
const MAX_COORDINATE: float = 10000.0
const CELL_ENCODING := "palette_author_planes_v1"
static func validate_canvas_request(data: Variant) -> bool:
@ -122,59 +127,93 @@ static func validate_canvas_state(data: Variant) -> bool:
if typeof(data) != TYPE_DICTIONARY:
return false
var value: Dictionary = data
if (
typeof(value.get("session_id")) != TYPE_STRING
or str(value["session_id"]).is_empty()
or str(value["session_id"]).length() > MAX_SESSION_ID_LENGTH
or typeof(value.get("canvas_id")) != TYPE_STRING
or str(value["canvas_id"]).is_empty()
or str(value["canvas_id"]).length() > MAX_CANVAS_ID_LENGTH
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("revision")) != TYPE_INT
or int(value["revision"]) < 0
or typeof(value.get("guide_visible", true)) != TYPE_BOOL
or typeof(value.get("finalized", false)) != TYPE_BOOL
or typeof(value.get("layer", 0)) != TYPE_INT
or int(value.get("layer", 0)) < 0
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
):
if not validate_canvas_metadata(value):
return false
var cells: Array = value["cells"]
var grid_width: int = int(value["width"])
var grid_height: int = int(value["height"])
if cells.size() > grid_width * grid_height:
return false
for cell_value: Variant in cells:
if (
not validate_authoritative_cell(cell_value)
or int((cell_value as Dictionary)["x"]) >= grid_width
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:
if not participants.is_empty() and str(value["creator_fingerprint"]) not in participants:
return false
if typeof(value.get("cells")) == TYPE_ARRAY:
var cells: Array = value["cells"]
var grid_width: int = int(value["width"])
var grid_height: int = int(value["height"])
if cells.size() > grid_width * grid_height:
return false
for cell_value: Variant in cells:
if str((cell_value as Dictionary)["author_fingerprint"]) not in participants:
if (
not validate_authoritative_cell(cell_value)
or int((cell_value as Dictionary)["x"]) >= grid_width
or int((cell_value as Dictionary)["y"]) >= grid_height
):
return false
if (
not participants.is_empty()
and str((cell_value as Dictionary)["author_fingerprint"])
not in participants
):
return false
return true
if (
str(value.get("cell_encoding", "")) != CELL_ENCODING
or typeof(value.get("cell_colors")) != TYPE_PACKED_BYTE_ARRAY
or typeof(value.get("cell_authors")) != TYPE_PACKED_INT32_ARRAY
or participants.is_empty()
):
return false
var colors: PackedByteArray = value["cell_colors"]
var authors: PackedInt32Array = value["cell_authors"]
var expected_size: int = int(value["width"]) * int(value["height"])
if colors.size() != expected_size or authors.size() != expected_size:
return false
for index: int in expected_size:
var color_index: int = colors[index]
var author_slot: int = authors[index]
if color_index == 0:
if author_slot != 0:
return false
elif (
color_index > SurfaceDrawingPalette.COLORS.size()
or author_slot <= 0
or author_slot > participants.size()
):
return false
return true
static func validate_canvas_metadata(data: Variant) -> bool:
if typeof(data) != TYPE_DICTIONARY:
return false
var value: Dictionary = data
return (
typeof(value.get("session_id")) == TYPE_STRING
and not str(value["session_id"]).is_empty()
and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
and typeof(value.get("canvas_id")) == TYPE_STRING
and not str(value["canvas_id"]).is_empty()
and str(value["canvas_id"]).length() <= MAX_CANVAS_ID_LENGTH
and _valid_vector(value.get("origin"))
and _valid_vector(value.get("normal"))
and _valid_vector(value.get("tangent"))
and typeof(value.get("width")) == TYPE_INT
and int(value["width"]) in GRID_SIZES
and typeof(value.get("height")) == TYPE_INT
and int(value["height"]) == int(value["width"])
and typeof(value.get("cell_size")) in [TYPE_FLOAT, TYPE_INT]
and is_equal_approx(float(value["cell_size"]), CELL_SIZE)
and typeof(value.get("revision")) == TYPE_INT
and int(value["revision"]) >= 0
and typeof(value.get("guide_visible", true)) == TYPE_BOOL
and typeof(value.get("finalized", false)) == TYPE_BOOL
and typeof(value.get("layer", 0)) == TYPE_INT
and int(value.get("layer", 0)) >= 0
and NetworkIdentityCrypto.valid_fingerprint(
value.get("creator_fingerprint")
)
and _valid_participant_fingerprints(
value.get("participant_fingerprints", [])
)
)
static func validate_guide_update(data: Variant) -> bool:
if typeof(data) != TYPE_DICTIONARY:
return false