Optimize finalized surface artwork
This commit is contained in:
parent
82938d3d57
commit
f2239d248a
14 changed files with 1254 additions and 305 deletions
|
|
@ -284,6 +284,35 @@ godot --headless --path . --script scripts/normalize_texture_imports.gd \
|
|||
godot --headless --path . --script tests/texture_sampling_validation.gd
|
||||
```
|
||||
|
||||
### Surface artwork runtime
|
||||
|
||||
Art Kit cells are authoritative palette and author planes, not per-cell nodes
|
||||
or dictionaries. Keep `SurfaceDrawingCellBuffer` as the live representation;
|
||||
materialize the legacy cell-array view only for inspection and tests. Finished
|
||||
artwork uses a full-resolution nearest-neighbor texture on a coarse,
|
||||
surface-conforming mesh. It discards guide and brush transforms, receives a
|
||||
distance visibility range, and unloads its render node when far from the local
|
||||
player while retaining compact session state.
|
||||
|
||||
`surface_drawing_v3` snapshots carry packed palette and participant-index
|
||||
planes. The host sends late-join snapshots over multiple frames, prioritizing
|
||||
active and nearby canvases, so adding finished work must not restore a
|
||||
single-frame snapshot burst. Active editing and finalized artwork have
|
||||
separate bounded budgets; finalizing a canvas frees active-grid capacity.
|
||||
|
||||
The finalized budget holds 512 maximum-size (128 x 128) canvases. At the
|
||||
current 0.075-meter cell size, 441 canvases can tile the full 200 x 200-meter
|
||||
generated-world footprint. If the generated world grows, update the finalized
|
||||
canvas and cell budgets together and rerun the artwork performance validation.
|
||||
|
||||
Run both focused validations after changing this path:
|
||||
|
||||
```sh
|
||||
godot --headless --path . --script tests/surface_drawing_validation.gd
|
||||
godot --headless --path . \
|
||||
--script tests/surface_drawing_performance_validation.gd
|
||||
```
|
||||
|
||||
### Importing animalese voice sets
|
||||
|
||||
Animalese clips use one deterministic runtime level and format: -18 dBFS RMS
|
||||
|
|
|
|||
|
|
@ -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", "")))
|
||||
|
|
|
|||
284
drawing/surface_drawing_cell_buffer.gd
Normal file
284
drawing/surface_drawing_cell_buffer.gd
Normal 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
|
||||
1
drawing/surface_drawing_cell_buffer.gd.uid
Normal file
1
drawing/surface_drawing_cell_buffer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b4clw478edbnk
|
||||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const DRAWING_RELIABLE_CHANNEL: int = 14
|
|||
const FISHING_RELIABLE_CHANNEL: int = 16
|
||||
const HOME_RELIABLE_CHANNEL: int = 17
|
||||
const ENET_CHANNEL_COUNT: int = 18
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v3"
|
||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
||||
const WORLD_TIME_CAPABILITY: String = "world_time_v2"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ const ARTWORK_DIRECTORY_NAME: String = "artwork"
|
|||
const STAMP_ALPHA_THRESHOLD: float = 0.5
|
||||
const STAMP_COLOR_TOLERANCE: float = 0.012
|
||||
const INVALID_POINTER_SCREEN_POSITION := Vector2(-1.0, -1.0)
|
||||
const SNAPSHOT_CANVASES_PER_FRAME: int = 2
|
||||
# A maximum-size 128 x 128 canvas consumes this entire budget, preventing
|
||||
# late-join synchronization from building two large artwork textures at once.
|
||||
const SNAPSHOT_GRID_CELLS_PER_FRAME: int = 16384
|
||||
const FINALIZED_NODE_LOAD_DISTANCE: float = 108.0
|
||||
const FINALIZED_NODE_UNLOAD_DISTANCE: float = 128.0
|
||||
const FINALIZED_STREAM_INTERVAL_MSEC: int = 250
|
||||
const FINALIZED_NODE_LOADS_PER_PASS: int = 1
|
||||
|
||||
enum GuideAction {
|
||||
NONE,
|
||||
|
|
@ -49,7 +57,10 @@ var _art_unlocks: PlayerArtUnlocks
|
|||
var _data_root: PlayerDataRoot
|
||||
var _drawing_root: Node3D
|
||||
var _canvas_states: Dictionary[String, Dictionary] = {}
|
||||
var _canvas_cells: Dictionary[String, SurfaceDrawingCellBuffer] = {}
|
||||
var _canvas_nodes: Dictionary[String, SurfaceDrawingCanvas] = {}
|
||||
var _snapshot_queues: Dictionary[int, Array] = {}
|
||||
var _last_finalized_stream_msec: int = 0
|
||||
var _selected_canvas_id: String = ""
|
||||
var _hovered_canvas_id: String = ""
|
||||
var _last_hovered_canvas_id: String = ""
|
||||
|
|
@ -753,8 +764,8 @@ func get_canvas_count() -> int:
|
|||
|
||||
func get_painted_cell_count() -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
count += (state.get("cells", []) as Array).size()
|
||||
for buffer: SurfaceDrawingCellBuffer in _canvas_cells.values():
|
||||
count += buffer.get_painted_count()
|
||||
return count
|
||||
|
||||
|
||||
|
|
@ -807,7 +818,13 @@ func get_canvas_ids() -> Array[String]:
|
|||
|
||||
|
||||
func get_canvas_state(canvas_id: String) -> Dictionary:
|
||||
return Dictionary(_canvas_states.get(canvas_id, {})).duplicate(true)
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if state.is_empty() or buffer == null:
|
||||
return {}
|
||||
var result: Dictionary = state.duplicate(true)
|
||||
result["cells"] = buffer.to_cells()
|
||||
return result
|
||||
|
||||
|
||||
func is_stamp_mode() -> bool:
|
||||
|
|
@ -1029,6 +1046,8 @@ func _next_artwork_export_path(directory: String) -> String:
|
|||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_pump_snapshot_queues()
|
||||
_refresh_finalized_canvas_streaming()
|
||||
if not _active:
|
||||
return
|
||||
if not can_activate():
|
||||
|
|
@ -1313,8 +1332,8 @@ func _handle_canvas_request(peer_id: int, data: Dictionary) -> void:
|
|||
or not _peer_can_place_grid(peer_id, requested_size)
|
||||
or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES
|
||||
or _active_canvas_count() >= SurfaceDrawingProtocol.MAX_ACTIVE_CANVASES
|
||||
or _allocated_grid_cells() + requested_size * requested_size
|
||||
> SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS
|
||||
or _allocated_active_grid_cells() + requested_size * requested_size
|
||||
> SurfaceDrawingProtocol.MAX_ACTIVE_GRID_CELLS
|
||||
):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
|
|
@ -1400,8 +1419,10 @@ func _handle_stamp_request(peer_id: int, data: Dictionary) -> void:
|
|||
or not _accept_request(peer_id, str(data["request_id"]))
|
||||
or not _peer_can_stamp(peer_id, data)
|
||||
or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES
|
||||
or _allocated_grid_cells() + requested_size * requested_size
|
||||
> SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS
|
||||
or _finalized_canvas_count()
|
||||
>= SurfaceDrawingProtocol.MAX_FINALIZED_CANVASES
|
||||
or _allocated_finalized_grid_cells() + requested_size * requested_size
|
||||
> SurfaceDrawingProtocol.MAX_FINALIZED_GRID_CELLS
|
||||
):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
|
|
@ -1442,6 +1463,12 @@ func _handle_stamp_request(peer_id: int, data: Dictionary) -> void:
|
|||
var canvas_id: String = "%s-%d" % [
|
||||
_session.get_session_id().left(16), _canvas_sequence,
|
||||
]
|
||||
var stamp_colors: PackedByteArray = data["pixels"]
|
||||
var stamp_authors := PackedInt32Array()
|
||||
stamp_authors.resize(stamp_colors.size())
|
||||
for cell_key: int in stamp_colors.size():
|
||||
if stamp_colors[cell_key] > 0:
|
||||
stamp_authors[cell_key] = 1
|
||||
var state: Dictionary = {
|
||||
"session_id": _session.get_session_id(),
|
||||
"canvas_id": canvas_id,
|
||||
|
|
@ -1459,9 +1486,9 @@ func _handle_stamp_request(peer_id: int, data: Dictionary) -> void:
|
|||
"layer": _canvas_sequence,
|
||||
"creator_fingerprint": record.identity_fingerprint,
|
||||
"participant_fingerprints": [record.identity_fingerprint],
|
||||
"cells": _stamp_cells(
|
||||
data["pixels"], requested_size, record.identity_fingerprint
|
||||
),
|
||||
"cell_encoding": SurfaceDrawingProtocol.CELL_ENCODING,
|
||||
"cell_colors": stamp_colors,
|
||||
"cell_authors": stamp_authors,
|
||||
"stamp": true,
|
||||
}
|
||||
_apply_canvas_state(state)
|
||||
|
|
@ -1469,27 +1496,6 @@ func _handle_stamp_request(peer_id: int, data: Dictionary) -> void:
|
|||
_emit_hud_state("stamp placed • finalized artwork shared with everyone")
|
||||
|
||||
|
||||
func _stamp_cells(
|
||||
pixels: PackedByteArray,
|
||||
grid_size: int,
|
||||
author_fingerprint: String,
|
||||
) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var palette_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
|
||||
for cell_key: int in range(pixels.size()):
|
||||
var palette_index: int = pixels[cell_key]
|
||||
if palette_index <= 0:
|
||||
continue
|
||||
var cell_y: int = floori(float(cell_key) / float(grid_size))
|
||||
result.append({
|
||||
"x": cell_key % grid_size,
|
||||
"y": cell_y,
|
||||
"color_id": str(palette_ids[palette_index - 1]),
|
||||
"author_fingerprint": author_fingerprint,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
|
|
@ -1529,6 +1535,18 @@ func _handle_guide_request(peer_id: int, data: Dictionary) -> void:
|
|||
var finalized: bool = bool(data["finalized"])
|
||||
if was_finalized:
|
||||
return
|
||||
if (
|
||||
finalized
|
||||
and (
|
||||
_finalized_canvas_count()
|
||||
>= SurfaceDrawingProtocol.MAX_FINALIZED_CANVASES
|
||||
or _allocated_finalized_grid_cells()
|
||||
+ int(state["width"]) * int(state["height"])
|
||||
> SurfaceDrawingProtocol.MAX_FINALIZED_GRID_CELLS
|
||||
)
|
||||
):
|
||||
_emit_hud_state("finished artwork limit reached for this session")
|
||||
return
|
||||
var guide_visible: bool = bool(data["guide_visible"]) and not finalized
|
||||
if (
|
||||
bool(state.get("guide_visible", true)) == guide_visible
|
||||
|
|
@ -1774,18 +1792,23 @@ func _queue_overlapping_finished_mutations(
|
|||
).normalized()
|
||||
if normal.dot(target_normal) < 0.9:
|
||||
continue
|
||||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if (
|
||||
canvas == null
|
||||
or not canvas.contains_world_point(
|
||||
not _state_contains_world_point(
|
||||
state,
|
||||
world_position,
|
||||
SurfaceDrawingCanvas.SURFACE_SAMPLE_DEPTH + 0.05,
|
||||
)
|
||||
):
|
||||
continue
|
||||
var center_cell: Vector2i = canvas.cell_at_world_point(world_position)
|
||||
var center_cell: Vector2i = _state_cell_at_world_point(
|
||||
state, world_position
|
||||
)
|
||||
if center_cell.x < 0:
|
||||
continue
|
||||
var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["tangent"]
|
||||
).normalized()
|
||||
var bitangent: Vector3 = normal.cross(tangent).normalized()
|
||||
for y_offset: int in range(-1, 2):
|
||||
for x_offset: int in range(-1, 2):
|
||||
var cell := Vector2i(
|
||||
|
|
@ -1794,19 +1817,19 @@ func _queue_overlapping_finished_mutations(
|
|||
)
|
||||
if (
|
||||
cell.x < 0
|
||||
or cell.x >= canvas.grid_width
|
||||
or cell.x >= int(state["width"])
|
||||
or cell.y < 0
|
||||
or cell.y >= canvas.grid_height
|
||||
or cell.y >= int(state["height"])
|
||||
):
|
||||
continue
|
||||
var relative: Vector3 = (
|
||||
canvas.get_cell_world_position(cell.x, cell.y)
|
||||
_state_cell_position(state, cell.x, cell.y)
|
||||
- world_position
|
||||
)
|
||||
if (
|
||||
absf(relative.dot(canvas.get_surface_tangent()))
|
||||
absf(relative.dot(tangent))
|
||||
>= SurfaceDrawingProtocol.CELL_SIZE
|
||||
or absf(relative.dot(canvas.get_surface_bitangent()))
|
||||
or absf(relative.dot(bitangent))
|
||||
>= SurfaceDrawingProtocol.CELL_SIZE
|
||||
or _state_cell(state, cell.x, cell.y).is_empty()
|
||||
):
|
||||
|
|
@ -1861,11 +1884,9 @@ func _publish_cell_mutations(
|
|||
canvas_ids.sort()
|
||||
for canvas_id: String in canvas_ids:
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
if state.is_empty():
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if state.is_empty() or buffer == null:
|
||||
continue
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state["cells"], int(state["width"])
|
||||
)
|
||||
var canvas_mutations: Dictionary = mutations[canvas_id]
|
||||
var cell_keys: Array[int] = []
|
||||
for cell_key_value: Variant in canvas_mutations.keys():
|
||||
|
|
@ -1874,18 +1895,11 @@ func _publish_cell_mutations(
|
|||
var edits: Array[Dictionary] = []
|
||||
for cell_key: int in cell_keys:
|
||||
var edit: Dictionary = canvas_mutations[cell_key]
|
||||
if str(edit["color_id"]).is_empty():
|
||||
cells.erase(cell_key)
|
||||
else:
|
||||
cells[cell_key] = edit.duplicate(true)
|
||||
edits.append(edit.duplicate(true))
|
||||
if edits.is_empty():
|
||||
if edits.is_empty() or not buffer.apply_edits(edits):
|
||||
continue
|
||||
state["revision"] = int(state["revision"]) + 1
|
||||
state["cells"] = _sorted_cells(cells)
|
||||
state["participant_fingerprints"] = _merged_participants(
|
||||
state, {"edits": edits}
|
||||
)
|
||||
state["participant_fingerprints"] = buffer.get_participants()
|
||||
_canvas_states[canvas_id] = state
|
||||
var update: Dictionary = {
|
||||
"session_id": _session.get_session_id(),
|
||||
|
|
@ -1898,7 +1912,7 @@ func _publish_cell_mutations(
|
|||
}
|
||||
var local_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if local_canvas != null:
|
||||
local_canvas.apply_update(update)
|
||||
local_canvas.apply_buffered_update(update)
|
||||
_broadcast_canvas_update(update)
|
||||
_emit_artwork_changed()
|
||||
|
||||
|
|
@ -2068,8 +2082,13 @@ func request_canvas_snapshot(request_id: String) -> void:
|
|||
)
|
||||
):
|
||||
return
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
receive_canvas_state.rpc_id(sender_id, state)
|
||||
var canvas_ids: Array[String] = get_canvas_ids()
|
||||
canvas_ids.sort_custom(func(first: String, second: String) -> bool:
|
||||
return _snapshot_canvas_precedes(first, second, sender_id)
|
||||
)
|
||||
var queued: Array = []
|
||||
queued.assign(canvas_ids)
|
||||
_snapshot_queues[sender_id] = queued
|
||||
|
||||
|
||||
func _apply_canvas_state(data: Dictionary) -> void:
|
||||
|
|
@ -2085,12 +2104,21 @@ func _apply_canvas_state(data: Dictionary) -> void:
|
|||
and int(data["revision"]) <= int(previous_state["revision"])
|
||||
):
|
||||
return
|
||||
var buffer := SurfaceDrawingCellBuffer.new()
|
||||
if not buffer.configure_from_state(data, true):
|
||||
return
|
||||
var normalized_state: Dictionary = data.duplicate(true)
|
||||
normalized_state["participant_fingerprints"] = _state_participants(
|
||||
normalized_state
|
||||
)
|
||||
normalized_state.erase("cells")
|
||||
normalized_state.erase("cell_encoding")
|
||||
normalized_state.erase("cell_colors")
|
||||
normalized_state.erase("cell_authors")
|
||||
normalized_state["participant_fingerprints"] = buffer.get_participants()
|
||||
_canvas_states[canvas_id] = normalized_state
|
||||
_rebuild_canvas_node(normalized_state)
|
||||
_canvas_cells[canvas_id] = buffer
|
||||
if _should_load_canvas_node(normalized_state, FINALIZED_NODE_LOAD_DISTANCE):
|
||||
_rebuild_canvas_node(normalized_state, buffer)
|
||||
else:
|
||||
_unload_canvas_node(canvas_id)
|
||||
_refresh_stencil_visibility()
|
||||
_emit_artwork_changed()
|
||||
if (
|
||||
|
|
@ -2105,7 +2133,10 @@ func _apply_canvas_state(data: Dictionary) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _rebuild_canvas_node(data: Dictionary) -> void:
|
||||
func _rebuild_canvas_node(
|
||||
data: Dictionary,
|
||||
buffer: SurfaceDrawingCellBuffer,
|
||||
) -> void:
|
||||
var canvas_id: String = str(data["canvas_id"])
|
||||
var previous_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if previous_canvas != null:
|
||||
|
|
@ -2113,12 +2144,82 @@ func _rebuild_canvas_node(data: Dictionary) -> void:
|
|||
var canvas := SurfaceDrawingCanvas.new()
|
||||
canvas.name = "Drawing_%s" % canvas_id
|
||||
_drawing_root.add_child(canvas)
|
||||
if not canvas.setup(data, _relationships, SOLID_SURFACE_MASK):
|
||||
if not canvas.setup(data, _relationships, SOLID_SURFACE_MASK, buffer):
|
||||
canvas.queue_free()
|
||||
return
|
||||
_canvas_nodes[canvas_id] = canvas
|
||||
|
||||
|
||||
func _refresh_finalized_canvas_streaming() -> void:
|
||||
var now: int = Time.get_ticks_msec()
|
||||
if now - _last_finalized_stream_msec < FINALIZED_STREAM_INTERVAL_MSEC:
|
||||
return
|
||||
_last_finalized_stream_msec = now
|
||||
for canvas_id: String in _canvas_nodes.keys():
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
if (
|
||||
bool(state.get("finalized", false))
|
||||
and not _should_load_canvas_node(
|
||||
state, FINALIZED_NODE_UNLOAD_DISTANCE
|
||||
)
|
||||
):
|
||||
_unload_canvas_node(canvas_id)
|
||||
var candidates: Array[String] = []
|
||||
for canvas_id: String in _canvas_states:
|
||||
var state: Dictionary = _canvas_states[canvas_id]
|
||||
if (
|
||||
not _canvas_nodes.has(canvas_id)
|
||||
and _should_load_canvas_node(state, FINALIZED_NODE_LOAD_DISTANCE)
|
||||
):
|
||||
candidates.append(canvas_id)
|
||||
candidates.sort_custom(func(first: String, second: String) -> bool:
|
||||
return _canvas_distance_squared(first) < _canvas_distance_squared(second)
|
||||
)
|
||||
for index: int in mini(candidates.size(), FINALIZED_NODE_LOADS_PER_PASS):
|
||||
var canvas_id: String = candidates[index]
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if buffer != null:
|
||||
_rebuild_canvas_node(_canvas_states[canvas_id], buffer)
|
||||
|
||||
|
||||
func _should_load_canvas_node(state: Dictionary, distance: float) -> bool:
|
||||
if state.is_empty():
|
||||
return false
|
||||
if not bool(state.get("finalized", false)):
|
||||
return true
|
||||
if _local_player == null or not is_instance_valid(_local_player):
|
||||
return false
|
||||
return _local_player.global_position.distance_squared_to(
|
||||
SurfaceDrawingProtocol.array_to_vector(state["origin"])
|
||||
) <= distance * distance
|
||||
|
||||
|
||||
func _canvas_distance_squared(canvas_id: String) -> float:
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
if (
|
||||
state.is_empty()
|
||||
or _local_player == null
|
||||
or not is_instance_valid(_local_player)
|
||||
):
|
||||
return INF
|
||||
return _local_player.global_position.distance_squared_to(
|
||||
SurfaceDrawingProtocol.array_to_vector(state["origin"])
|
||||
)
|
||||
|
||||
|
||||
func _unload_canvas_node(canvas_id: String) -> void:
|
||||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if canvas != null:
|
||||
canvas.queue_free()
|
||||
_canvas_nodes.erase(canvas_id)
|
||||
if _selected_canvas_id == canvas_id:
|
||||
_selected_canvas_id = ""
|
||||
if _hovered_canvas_id == canvas_id:
|
||||
_hovered_canvas_id = ""
|
||||
if _last_hovered_canvas_id == canvas_id:
|
||||
_last_hovered_canvas_id = ""
|
||||
|
||||
|
||||
func _apply_canvas_update(data: Dictionary) -> void:
|
||||
if (
|
||||
not SurfaceDrawingProtocol.validate_canvas_update(data)
|
||||
|
|
@ -2127,12 +2228,14 @@ func _apply_canvas_update(data: Dictionary) -> void:
|
|||
return
|
||||
var canvas_id: String = str(data["canvas_id"])
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if state.is_empty() or canvas == null or int(data["revision"]) <= int(state["revision"]):
|
||||
if (
|
||||
state.is_empty()
|
||||
or buffer == null
|
||||
or int(data["revision"]) <= int(state["revision"])
|
||||
):
|
||||
return
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state["cells"], int(state["width"])
|
||||
)
|
||||
var grid_width: int = int(state["width"])
|
||||
var grid_height: int = int(state["height"])
|
||||
for edit_value: Variant in data["edits"]:
|
||||
|
|
@ -2141,18 +2244,13 @@ func _apply_canvas_update(data: Dictionary) -> void:
|
|||
var y: int = int(edit["y"])
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return
|
||||
var key: int = y * grid_width + x
|
||||
if str(edit["color_id"]).is_empty():
|
||||
cells.erase(key)
|
||||
else:
|
||||
cells[key] = edit.duplicate(true)
|
||||
if not buffer.apply_edits(data["edits"]):
|
||||
return
|
||||
state["revision"] = int(data["revision"])
|
||||
state["cells"] = _sorted_cells(cells)
|
||||
state["participant_fingerprints"] = _merged_participants(
|
||||
state, data
|
||||
)
|
||||
state["participant_fingerprints"] = buffer.get_participants()
|
||||
_canvas_states[canvas_id] = state
|
||||
canvas.apply_update(data)
|
||||
if canvas != null:
|
||||
canvas.apply_buffered_update(data)
|
||||
_emit_artwork_changed()
|
||||
|
||||
|
||||
|
|
@ -2167,7 +2265,6 @@ func _apply_guide_update(data: Dictionary) -> void:
|
|||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if (
|
||||
state.is_empty()
|
||||
or canvas == null
|
||||
or int(data["revision"]) <= int(state["revision"])
|
||||
):
|
||||
return
|
||||
|
|
@ -2179,8 +2276,13 @@ func _apply_guide_update(data: Dictionary) -> void:
|
|||
state["participant_fingerprints"] = _merged_participants(
|
||||
state, data
|
||||
)
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if buffer != null:
|
||||
buffer.ensure_participants(state["participant_fingerprints"])
|
||||
state["participant_fingerprints"] = buffer.get_participants()
|
||||
_canvas_states[canvas_id] = state
|
||||
canvas.apply_guide_update(data)
|
||||
if canvas != null:
|
||||
canvas.apply_guide_update(data)
|
||||
if _active:
|
||||
_emit_hud_state(
|
||||
"grid finalized • new grids may overlap its artwork"
|
||||
|
|
@ -2193,13 +2295,90 @@ func _apply_guide_update(data: Dictionary) -> void:
|
|||
|
||||
|
||||
func _broadcast_canvas_state(data: Dictionary) -> void:
|
||||
var network_state: Dictionary = _network_canvas_state(
|
||||
str(data.get("canvas_id", ""))
|
||||
)
|
||||
if network_state.is_empty():
|
||||
return
|
||||
for peer_id: int in _session.get_authenticated_peer_ids():
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
continue
|
||||
if _session.peer_supports_capability(
|
||||
peer_id, SurfaceDrawingProtocol.CAPABILITY
|
||||
):
|
||||
receive_canvas_state.rpc_id(peer_id, data)
|
||||
receive_canvas_state.rpc_id(peer_id, network_state)
|
||||
|
||||
|
||||
func _network_canvas_state(canvas_id: String) -> Dictionary:
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if state.is_empty() or buffer == null:
|
||||
return {}
|
||||
var result: Dictionary = state.duplicate(true)
|
||||
result.merge(buffer.to_compact_fields(), true)
|
||||
return result
|
||||
|
||||
|
||||
func _pump_snapshot_queues() -> void:
|
||||
if _session == null or not _session.is_host() or _snapshot_queues.is_empty():
|
||||
return
|
||||
var peer_ids: Array[int] = []
|
||||
peer_ids.assign(_snapshot_queues.keys())
|
||||
peer_ids.sort()
|
||||
for peer_id: int in peer_ids:
|
||||
if not _session.is_authenticated_peer(peer_id):
|
||||
_snapshot_queues.erase(peer_id)
|
||||
continue
|
||||
var queue: Array = _snapshot_queues.get(peer_id, [])
|
||||
var sent_count: int = 0
|
||||
var sent_grid_cells: int = 0
|
||||
while not queue.is_empty() and sent_count < SNAPSHOT_CANVASES_PER_FRAME:
|
||||
var canvas_id: String = str(queue.front())
|
||||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
if state.is_empty():
|
||||
queue.pop_front()
|
||||
continue
|
||||
var grid_cells: int = int(state["width"]) * int(state["height"])
|
||||
if (
|
||||
sent_count > 0
|
||||
and sent_grid_cells + grid_cells > SNAPSHOT_GRID_CELLS_PER_FRAME
|
||||
):
|
||||
break
|
||||
queue.pop_front()
|
||||
var network_state: Dictionary = _network_canvas_state(canvas_id)
|
||||
if network_state.is_empty():
|
||||
continue
|
||||
receive_canvas_state.rpc_id(peer_id, network_state)
|
||||
sent_count += 1
|
||||
sent_grid_cells += grid_cells
|
||||
if queue.is_empty():
|
||||
_snapshot_queues.erase(peer_id)
|
||||
else:
|
||||
_snapshot_queues[peer_id] = queue
|
||||
|
||||
|
||||
func _snapshot_canvas_precedes(
|
||||
first_id: String,
|
||||
second_id: String,
|
||||
peer_id: int,
|
||||
) -> bool:
|
||||
var first: Dictionary = _canvas_states.get(first_id, {})
|
||||
var second: Dictionary = _canvas_states.get(second_id, {})
|
||||
var first_finalized: bool = bool(first.get("finalized", false))
|
||||
var second_finalized: bool = bool(second.get("finalized", false))
|
||||
if first_finalized != second_finalized:
|
||||
return not first_finalized
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
var first_distance: float = avatar.global_position.distance_squared_to(
|
||||
SurfaceDrawingProtocol.array_to_vector(first.get("origin", []))
|
||||
)
|
||||
var second_distance: float = avatar.global_position.distance_squared_to(
|
||||
SurfaceDrawingProtocol.array_to_vector(second.get("origin", []))
|
||||
)
|
||||
if not is_equal_approx(first_distance, second_distance):
|
||||
return first_distance < second_distance
|
||||
return int(first.get("layer", 0)) < int(second.get("layer", 0))
|
||||
|
||||
|
||||
func _broadcast_canvas_update(data: Dictionary) -> void:
|
||||
|
|
@ -2285,6 +2464,61 @@ func _state_cell_position(state: Dictionary, x: int, y: int) -> Vector3:
|
|||
)
|
||||
|
||||
|
||||
func _state_contains_world_point(
|
||||
state: Dictionary,
|
||||
world_point: Vector3,
|
||||
tolerance: float = 0.3,
|
||||
) -> bool:
|
||||
var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"])
|
||||
var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["normal"]
|
||||
).normalized()
|
||||
var relative: Vector3 = world_point - origin
|
||||
if absf(relative.dot(normal)) > tolerance:
|
||||
return false
|
||||
var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["tangent"]
|
||||
).normalized()
|
||||
var bitangent: Vector3 = normal.cross(tangent).normalized()
|
||||
var half_width: float = (
|
||||
float(state["width"]) * float(state["cell_size"]) * 0.5
|
||||
)
|
||||
var half_height: float = (
|
||||
float(state["height"]) * float(state["cell_size"]) * 0.5
|
||||
)
|
||||
var horizontal: float = relative.dot(tangent)
|
||||
var vertical: float = relative.dot(bitangent)
|
||||
return (
|
||||
horizontal >= -half_width
|
||||
and horizontal < half_width
|
||||
and vertical >= -half_height
|
||||
and vertical < half_height
|
||||
)
|
||||
|
||||
|
||||
func _state_cell_at_world_point(
|
||||
state: Dictionary,
|
||||
world_point: Vector3,
|
||||
) -> Vector2i:
|
||||
var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"])
|
||||
var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["normal"]
|
||||
).normalized()
|
||||
var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["tangent"]
|
||||
).normalized()
|
||||
var bitangent: Vector3 = normal.cross(tangent).normalized()
|
||||
var relative: Vector3 = world_point - origin
|
||||
var size: float = float(state["cell_size"])
|
||||
var half_width: float = float(state["width"]) * size * 0.5
|
||||
var half_height: float = float(state["height"]) * size * 0.5
|
||||
var x: int = floori((relative.dot(tangent) + half_width) / size)
|
||||
var y: int = floori((relative.dot(bitangent) + half_height) / size)
|
||||
if x < 0 or x >= int(state["width"]) or y < 0 or y >= int(state["height"]):
|
||||
return Vector2i(-1, -1)
|
||||
return Vector2i(x, y)
|
||||
|
||||
|
||||
func _overlaps_existing_canvas(
|
||||
origin: Vector3,
|
||||
normal: Vector3,
|
||||
|
|
@ -2335,10 +2569,27 @@ func _active_canvas_count() -> int:
|
|||
return count
|
||||
|
||||
|
||||
func _allocated_grid_cells() -> int:
|
||||
func _finalized_canvas_count() -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
count += int(state.get("width", 0)) * int(state.get("height", 0))
|
||||
if bool(state.get("finalized", false)):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _allocated_active_grid_cells() -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
if not bool(state.get("finalized", false)):
|
||||
count += int(state.get("width", 0)) * int(state.get("height", 0))
|
||||
return count
|
||||
|
||||
|
||||
func _allocated_finalized_grid_cells() -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
if bool(state.get("finalized", false)):
|
||||
count += int(state.get("width", 0)) * int(state.get("height", 0))
|
||||
return count
|
||||
|
||||
|
||||
|
|
@ -2678,30 +2929,10 @@ func _new_request_id(prefix: String) -> String:
|
|||
return "%s-%d-%d" % [prefix, Time.get_ticks_msec(), _request_sequence]
|
||||
|
||||
|
||||
func _cells_by_key(
|
||||
values: Array,
|
||||
grid_width: int,
|
||||
) -> Dictionary[int, Dictionary]:
|
||||
var result: Dictionary[int, Dictionary] = {}
|
||||
for value: Variant in values:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var cell: Dictionary = value
|
||||
var key: int = (
|
||||
int(cell.get("y", -1)) * grid_width
|
||||
+ int(cell.get("x", -1))
|
||||
)
|
||||
result[key] = cell.duplicate(true)
|
||||
return result
|
||||
|
||||
|
||||
func _state_cell(state: Dictionary, x: int, y: int) -> Dictionary:
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state.get("cells", []), int(state["width"])
|
||||
)
|
||||
return Dictionary(
|
||||
cells.get(_cell_key_for_state(state, x, y), {})
|
||||
).duplicate(true)
|
||||
var canvas_id: String = str(state.get("canvas_id", ""))
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
return buffer.get_cell_value(x, y) if buffer != null else {}
|
||||
|
||||
|
||||
func _register_canvas_participant(
|
||||
|
|
@ -2721,6 +2952,9 @@ func _register_canvas_participant(
|
|||
participants.append(fingerprint)
|
||||
state["participant_fingerprints"] = participants
|
||||
_canvas_states[canvas_id] = state
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(canvas_id)
|
||||
if buffer != null:
|
||||
buffer.ensure_participants(participants)
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -2740,17 +2974,17 @@ func _merged_participants(
|
|||
|
||||
|
||||
func _state_participants(state: Dictionary) -> Array[String]:
|
||||
var buffer: SurfaceDrawingCellBuffer = _canvas_cells.get(
|
||||
str(state.get("canvas_id", ""))
|
||||
)
|
||||
if buffer != null:
|
||||
return buffer.get_participants()
|
||||
var result: Array[String] = []
|
||||
_append_participant(
|
||||
result, str(state.get("creator_fingerprint", ""))
|
||||
)
|
||||
for value: Variant in state.get("participant_fingerprints", []):
|
||||
_append_participant(result, str(value))
|
||||
for cell_value: Variant in state.get("cells", []):
|
||||
var cell: Dictionary = cell_value
|
||||
_append_participant(
|
||||
result, str(cell.get("author_fingerprint", ""))
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -2804,15 +3038,6 @@ func _mutation_key(canvas_id: String, x: int, y: int) -> String:
|
|||
return "%s:%d:%d" % [canvas_id, x, y]
|
||||
|
||||
|
||||
func _sorted_cells(values: Dictionary[int, Dictionary]) -> Array[Dictionary]:
|
||||
var keys: Array[int] = values.keys()
|
||||
keys.sort()
|
||||
var result: Array[Dictionary] = []
|
||||
for key: int in keys:
|
||||
result.append(values[key].duplicate(true))
|
||||
return result
|
||||
|
||||
|
||||
func _normalized_or(value: Variant, fallback: Vector3) -> Vector3:
|
||||
var vector: Vector3 = value if typeof(value) == TYPE_VECTOR3 else fallback
|
||||
return fallback if vector.is_zero_approx() else vector.normalized()
|
||||
|
|
@ -2850,6 +3075,7 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
_peer_request_ids.erase(peer_id)
|
||||
_stroke_history_by_peer.erase(peer_id)
|
||||
_peer_art_entitlements.erase(peer_id)
|
||||
_snapshot_queues.erase(peer_id)
|
||||
|
||||
|
||||
func _on_local_art_entitlement_changed() -> void:
|
||||
|
|
@ -2898,6 +3124,7 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
|
||||
func _clear_session_artwork_state() -> void:
|
||||
_canvas_states.clear()
|
||||
_canvas_cells.clear()
|
||||
for canvas: SurfaceDrawingCanvas in _canvas_nodes.values():
|
||||
canvas.queue_free()
|
||||
_canvas_nodes.clear()
|
||||
|
|
@ -2908,6 +3135,7 @@ func _clear_session_artwork_state() -> void:
|
|||
_stroke_history_by_peer.clear()
|
||||
_cell_last_stroke.clear()
|
||||
_last_local_stroke_id = ""
|
||||
_snapshot_queues.clear()
|
||||
_hide_previews()
|
||||
_emit_artwork_changed()
|
||||
|
||||
|
|
@ -2932,16 +3160,8 @@ func _apply_artwork_fingerprint_clear(fingerprint: String) -> int:
|
|||
|
||||
func _remove_canvas_state(canvas_id: String) -> void:
|
||||
_canvas_states.erase(canvas_id)
|
||||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if canvas != null:
|
||||
canvas.queue_free()
|
||||
_canvas_nodes.erase(canvas_id)
|
||||
if _selected_canvas_id == canvas_id:
|
||||
_selected_canvas_id = ""
|
||||
if _hovered_canvas_id == canvas_id:
|
||||
_hovered_canvas_id = ""
|
||||
if _last_hovered_canvas_id == canvas_id:
|
||||
_last_hovered_canvas_id = ""
|
||||
_canvas_cells.erase(canvas_id)
|
||||
_unload_canvas_node(canvas_id)
|
||||
|
||||
|
||||
func _emit_artwork_changed() -> void:
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ readonly -a QUICK_TESTS=(
|
|||
"tests/player_experience_validation.gd"
|
||||
"tests/shoreline_ambience_validation.gd"
|
||||
"tests/surface_drawing_validation.gd"
|
||||
"tests/surface_drawing_performance_validation.gd"
|
||||
"tests/surface_drawing_texture_renderer_validation.gd"
|
||||
"tests/tackle_order_validation.gd"
|
||||
"tests/terrain_biome_validation.gd"
|
||||
|
|
|
|||
178
tests/surface_drawing_performance_validation.gd
Normal file
178
tests/surface_drawing_performance_validation.gd
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
extends SceneTree
|
||||
|
||||
const CANVAS_COUNT: int = 10
|
||||
const GRID_SIZE: int = 128
|
||||
const MAX_SETUP_MSEC: float = 2500.0
|
||||
const MAX_STATIC_MEMORY_MIB: float = 32.0
|
||||
const FINGERPRINT_A := (
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
const FINGERPRINT_B := (
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_finalized_capacity()
|
||||
_validate_dense_cells_and_author_index()
|
||||
var drawing_root := Node3D.new()
|
||||
root.add_child(drawing_root)
|
||||
var colors := PackedByteArray()
|
||||
colors.resize(GRID_SIZE * GRID_SIZE)
|
||||
colors.fill(1)
|
||||
var authors := PackedInt32Array()
|
||||
authors.resize(GRID_SIZE * GRID_SIZE)
|
||||
authors.fill(1)
|
||||
var memory_before: int = OS.get_static_memory_usage()
|
||||
var started_usec: int = Time.get_ticks_usec()
|
||||
for canvas_index: int in CANVAS_COUNT:
|
||||
var canvas := SurfaceDrawingCanvas.new()
|
||||
drawing_root.add_child(canvas)
|
||||
var state: Dictionary = _compact_state(
|
||||
"canvas-%d" % canvas_index,
|
||||
canvas_index,
|
||||
colors,
|
||||
authors,
|
||||
)
|
||||
assert(SurfaceDrawingProtocol.validate_canvas_state(state))
|
||||
assert(canvas.setup(state, null))
|
||||
assert(canvas.get_rendered_pixel_count() == GRID_SIZE * GRID_SIZE)
|
||||
var artwork := canvas.get("_pixel_instance") as MeshInstance3D
|
||||
var arrays: Array = artwork.mesh.surface_get_arrays(0)
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
assert(
|
||||
vertices.size()
|
||||
<= (SurfaceDrawingCanvas.FINALIZED_MAX_SUBDIVISIONS + 1) ** 2
|
||||
)
|
||||
var setup_msec: float = (
|
||||
float(Time.get_ticks_usec() - started_usec) / 1000.0
|
||||
)
|
||||
var memory_mib: float = (
|
||||
float(OS.get_static_memory_usage() - memory_before) / 1048576.0
|
||||
)
|
||||
assert(
|
||||
setup_msec <= MAX_SETUP_MSEC,
|
||||
"finalized artwork setup took %.2f ms" % setup_msec,
|
||||
)
|
||||
assert(
|
||||
memory_mib <= MAX_STATIC_MEMORY_MIB,
|
||||
"finalized artwork retained %.2f MiB" % memory_mib,
|
||||
)
|
||||
print(
|
||||
"Surface drawing performance validation: PASS (%.2f ms, %.2f MiB)"
|
||||
% [setup_msec, memory_mib]
|
||||
)
|
||||
drawing_root.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_finalized_capacity() -> void:
|
||||
var world_scene := load(
|
||||
"res://world/generation/generated_world_region.tscn"
|
||||
) as PackedScene
|
||||
assert(world_scene != null)
|
||||
var world_region: Node = world_scene.instantiate()
|
||||
var generator := world_region.get_node(
|
||||
"Terrain/TerrainChunkGenerator"
|
||||
) as TerrainChunkGenerator
|
||||
assert(generator != null and generator.catalog != null)
|
||||
var canvas_extent: float = (
|
||||
float(SurfaceDrawingProtocol.MAX_GRID_SIZE)
|
||||
* SurfaceDrawingProtocol.CELL_SIZE
|
||||
)
|
||||
var required_columns: int = ceili(
|
||||
float(generator.grid_size.x) * generator.catalog.chunk_size
|
||||
/ canvas_extent
|
||||
)
|
||||
var required_rows: int = ceili(
|
||||
float(generator.grid_size.y) * generator.catalog.chunk_size
|
||||
/ canvas_extent
|
||||
)
|
||||
var required_canvases: int = required_columns * required_rows
|
||||
var cells_per_canvas: int = (
|
||||
SurfaceDrawingProtocol.MAX_GRID_SIZE
|
||||
* SurfaceDrawingProtocol.MAX_GRID_SIZE
|
||||
)
|
||||
assert(
|
||||
SurfaceDrawingProtocol.MAX_FINALIZED_CANVASES >= required_canvases,
|
||||
"finalized canvas budget cannot tile the generated world",
|
||||
)
|
||||
assert(
|
||||
SurfaceDrawingProtocol.MAX_FINALIZED_GRID_CELLS
|
||||
>= required_canvases * cells_per_canvas,
|
||||
"finalized cell budget cannot tile the generated world",
|
||||
)
|
||||
world_region.free()
|
||||
|
||||
|
||||
func _validate_dense_cells_and_author_index() -> void:
|
||||
var state: Dictionary = {
|
||||
"session_id": "dense-cell-test",
|
||||
"canvas_id": "dense-cell-canvas",
|
||||
"origin": [0.0, 0.0, 0.0],
|
||||
"normal": [0.0, 1.0, 0.0],
|
||||
"tangent": [1.0, 0.0, 0.0],
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"cell_size": SurfaceDrawingProtocol.CELL_SIZE,
|
||||
"revision": 1,
|
||||
"guide_visible": false,
|
||||
"finalized": true,
|
||||
"layer": 1,
|
||||
"creator_fingerprint": FINGERPRINT_A,
|
||||
"participant_fingerprints": [FINGERPRINT_A, FINGERPRINT_B],
|
||||
"cells": [
|
||||
{"x": 1, "y": 2, "color_id": "coral", "author_fingerprint": FINGERPRINT_A},
|
||||
{"x": 3, "y": 4, "color_id": "blue", "author_fingerprint": FINGERPRINT_B},
|
||||
],
|
||||
}
|
||||
var buffer := SurfaceDrawingCellBuffer.new()
|
||||
assert(buffer.configure_from_state(state))
|
||||
assert(buffer.get_painted_count() == 2)
|
||||
assert(buffer.get_author_cell_keys(FINGERPRINT_A) == PackedInt32Array([33]))
|
||||
assert(buffer.get_author_cell_keys(FINGERPRINT_B) == PackedInt32Array([67]))
|
||||
var compact: Dictionary = state.duplicate(true)
|
||||
compact.erase("cells")
|
||||
compact.merge(buffer.to_compact_fields(), true)
|
||||
assert(SurfaceDrawingProtocol.validate_canvas_state(compact))
|
||||
var restored := SurfaceDrawingCellBuffer.new()
|
||||
assert(restored.configure_from_state(compact))
|
||||
assert(restored.to_cells() == buffer.to_cells())
|
||||
assert(restored.apply_edits([
|
||||
{"x": 1, "y": 2, "color_id": "", "author_fingerprint": ""},
|
||||
]))
|
||||
assert(restored.get_author_cell_keys(FINGERPRINT_A).is_empty())
|
||||
assert(restored.get_painted_count() == 1)
|
||||
|
||||
|
||||
func _compact_state(
|
||||
canvas_id: String,
|
||||
layer: int,
|
||||
colors: PackedByteArray,
|
||||
authors: PackedInt32Array,
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"session_id": "performance-test",
|
||||
"canvas_id": canvas_id,
|
||||
"origin": [float(layer) * 12.0, 0.0, 0.0],
|
||||
"normal": [0.0, 1.0, 0.0],
|
||||
"tangent": [1.0, 0.0, 0.0],
|
||||
"width": GRID_SIZE,
|
||||
"height": GRID_SIZE,
|
||||
"cell_size": SurfaceDrawingProtocol.CELL_SIZE,
|
||||
"revision": 1,
|
||||
"guide_visible": false,
|
||||
"finalized": true,
|
||||
"layer": layer,
|
||||
"creator_fingerprint": FINGERPRINT_A,
|
||||
"participant_fingerprints": [FINGERPRINT_A],
|
||||
"cell_encoding": SurfaceDrawingProtocol.CELL_ENCODING,
|
||||
"cell_colors": colors,
|
||||
"cell_authors": authors,
|
||||
}
|
||||
1
tests/surface_drawing_performance_validation.gd.uid
Normal file
1
tests/surface_drawing_performance_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://g73q2xuy33nm
|
||||
|
|
@ -94,6 +94,16 @@ func _run() -> void:
|
|||
if child is MeshInstance3D:
|
||||
artwork_mesh_count += 1
|
||||
assert(artwork_mesh_count == 1)
|
||||
var finalized_instance := canvas.get(
|
||||
"_pixel_instance"
|
||||
) as MeshInstance3D
|
||||
var finalized_material := finalized_instance.mesh.surface_get_material(
|
||||
0
|
||||
) as StandardMaterial3D
|
||||
assert(
|
||||
finalized_material.albedo_texture
|
||||
in persistent_textures
|
||||
)
|
||||
assert(canvas.get_export_image().get_size() == Vector2i(32, 32))
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
|
|
|
|||
|
|
@ -44,9 +44,24 @@ func _validate_protocol_bounds() -> void:
|
|||
var grid_height: int = SurfaceDrawingProtocol.GRID_HEIGHT
|
||||
var grid_sizes: Variant = SurfaceDrawingProtocol.GRID_SIZES
|
||||
var cell_size: float = SurfaceDrawingProtocol.CELL_SIZE
|
||||
assert(
|
||||
str(SurfaceDrawingProtocol.CAPABILITY)
|
||||
== NetworkProtocol.SURFACE_DRAWING_CAPABILITY
|
||||
)
|
||||
assert(grid_width == 16)
|
||||
assert(grid_height == 16)
|
||||
assert(grid_sizes == [16, 32, 64, 128])
|
||||
assert(
|
||||
SurfaceDrawingProtocol.MAX_CANVASES
|
||||
== SurfaceDrawingProtocol.MAX_ACTIVE_CANVASES
|
||||
+ SurfaceDrawingProtocol.MAX_FINALIZED_CANVASES
|
||||
)
|
||||
assert(
|
||||
SurfaceDrawingProtocol.MAX_FINALIZED_GRID_CELLS
|
||||
>= SurfaceDrawingProtocol.MAX_FINALIZED_CANVASES
|
||||
* SurfaceDrawingProtocol.MAX_GRID_SIZE
|
||||
* SurfaceDrawingProtocol.MAX_GRID_SIZE
|
||||
)
|
||||
assert(is_equal_approx(cell_size, 0.075))
|
||||
assert(is_equal_approx(
|
||||
float(grid_width) * cell_size,
|
||||
|
|
@ -242,8 +257,12 @@ func _validate_canvas_geometry_and_collaboration() -> void:
|
|||
assert(not canvas.is_guide_visible())
|
||||
assert(canvas.is_finalized())
|
||||
assert(not canvas.has_guide_geometry())
|
||||
assert(pixel_instance.mesh == persistent_mesh)
|
||||
assert(pixel_material.albedo_texture == persistent_texture)
|
||||
var finalized_instance := canvas.get("_pixel_instance") as MeshInstance3D
|
||||
var finalized_material := finalized_instance.mesh.surface_get_material(
|
||||
0
|
||||
) as StandardMaterial3D
|
||||
assert(finalized_instance.mesh != persistent_mesh)
|
||||
assert(finalized_material.albedo_texture == persistent_texture)
|
||||
await process_frame
|
||||
var finished_pixel_scale: float = canvas.get_rendered_pixel_size()
|
||||
assert(
|
||||
|
|
@ -265,8 +284,8 @@ func _validate_canvas_geometry_and_collaboration() -> void:
|
|||
}],
|
||||
}
|
||||
assert(canvas.apply_update(second_update))
|
||||
assert(pixel_instance.mesh == persistent_mesh)
|
||||
assert(pixel_material.albedo_texture == persistent_texture)
|
||||
assert(finalized_instance == canvas.get("_pixel_instance"))
|
||||
assert(finalized_material.albedo_texture == persistent_texture)
|
||||
var cells: Array[Dictionary] = canvas.get_authoritative_cells()
|
||||
assert(cells.size() == 1)
|
||||
assert(cells[0]["color_id"] == "blue")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue