Add artwork export and stamping

This commit is contained in:
Alexander Sellite 2026-08-21 21:01:21 -04:00
parent ba35db1015
commit 8107e2d89c
21 changed files with 1727 additions and 105 deletions

View file

@ -481,6 +481,8 @@ func _run() -> void:
var hide_button := toolbar.get_node("%HideGuideButton") as Button
var restore_button := toolbar.get_node("%RestoreGuideButton") as Button
var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button
var export_button := toolbar.get_node("%ExportButton") as Button
var stamp_button := toolbar.get_node("%StampButton") as Button
assert(
mode_button != null
and eraser_button != null
@ -488,7 +490,13 @@ func _run() -> void:
and hide_button != null
and restore_button != null
and finalize_button != null
and export_button != null
and stamp_button != null
)
assert(export_button.text == "png")
assert(export_button.tooltip_text == "export aimed artwork as PNG")
assert(stamp_button.text == "stamp")
assert(stamp_button.disabled)
assert(toolbar.get_node_or_null("%CloseButton") == null)
var expected_toolbar_icons: Dictionary[Button, String] = {
mode_button: "/art/art_kit_marker.png",
@ -511,6 +519,8 @@ func _run() -> void:
hide_button,
restore_button,
finalize_button,
export_button,
stamp_button,
]:
assert(icon_button.custom_minimum_size == Vector2(48, 44))
assert(icon_button.get_theme_constant("icon_max_width") == 40)
@ -521,7 +531,9 @@ func _run() -> void:
assert(is_equal_approx(icon_style.content_margin_top, 2.0))
assert(mode_button.custom_minimum_size == Vector2(48, 48))
assert(mode_button.get_theme_constant("icon_max_width") == 40)
assert(mode_button.get_index() > finalize_button.get_index())
assert(export_button.get_index() > finalize_button.get_index())
assert(stamp_button.get_index() > export_button.get_index())
assert(mode_button.get_index() > stamp_button.get_index())
assert(hide_button.button_group != null)
assert(hide_button.button_group == restore_button.button_group)
assert(hide_button.button_group == finalize_button.button_group)
@ -537,6 +549,8 @@ func _run() -> void:
hide_button,
restore_button,
finalize_button,
export_button,
stamp_button,
]:
assert(pointer_only_control.focus_mode == Control.FOCUS_NONE)
assert(brush_option.get_popup().unfocusable)
@ -639,13 +653,31 @@ func _run() -> void:
)) == 24)
for product_id: StringName in [
&"marker_ocean_teal", &"brush_4x", &"grid_128x",
&"marker_ocean_teal",
&"brush_4x",
&"grid_128x",
PlayerArtUnlocks.STAMP_PRODUCT_ID,
]:
assert(player.art_unlocks.unlock_product(product_id))
await process_frame
assert(not (color_buttons[&"ocean_teal"] as Button).disabled)
assert(not brush_option.get_popup().is_item_disabled(3))
assert(not grid_option.get_popup().is_item_disabled(3))
assert(not stamp_button.disabled)
var stamp_library_panel := toolbar.get_node(
"%StampLibraryPanel"
) as PanelContainer
var stamp_library_close := toolbar.get_node(
"%StampLibraryClose"
) as Button
assert(stamp_library_panel != null and not stamp_library_panel.visible)
stamp_button.pressed.emit()
assert(stamp_library_panel.visible)
var library_pointer := InputEventMouseMotion.new()
library_pointer.position = stamp_library_panel.get_global_rect().get_center()
assert(toolbar.owns_pointer_event(library_pointer))
stamp_library_close.pressed.emit()
assert(not stamp_library_panel.visible)
assert(not locked_color_lock.visible)
assert(str(brush_option.get_item_icon(3).get_meta(
&"channel_mask_source", ""

View file

@ -487,7 +487,7 @@ func _test_host_art_shop_purchase(
for _frame: int in 4:
await physics_frame
var required_balance: int = (
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE * 3
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE * 4
)
if player.wallet.get_balance() < required_balance:
assert(player.wallet.credit(required_balance - player.wallet.get_balance()))
@ -505,7 +505,10 @@ func _test_host_art_shop_purchase(
assert(art_item.icon.resource_path.ends_with("/art/art_kit.png"))
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
for product_id: StringName in [
&"marker_ocean_teal", &"brush_2x", &"grid_32x",
&"marker_ocean_teal",
&"brush_2x",
&"grid_32x",
PlayerArtUnlocks.STAMP_PRODUCT_ID,
]:
_shop_result.clear()
assert(not shop_service.request_art_upgrade(product_id).is_empty())
@ -844,7 +847,9 @@ func _test_fishing_shop_sale_ui(
for child: Node in shop.get_node("%SuppliesList").get_children():
if child is Label:
stock_sections.append((child as Label).text)
assert(stock_sections == ["art kit", "markers", "brushes", "grids"])
assert(stock_sections == [
"art kit", "markers", "brushes", "grids", "stamps",
])
var marker_icons := shop.find_children(
"MarkerIcon", "TextureRect", true, false
)
@ -903,6 +908,17 @@ func _test_fishing_shop_sale_ui(
)
)
assert(found_art_upgrade_icons == expected_art_upgrade_icons.size())
var stamp_button: Button
for upgrade_node: Node in shop.find_children("*", "Button", true, false):
var upgrade_button := upgrade_node as Button
if StringName(str(upgrade_button.get_meta(&"art_product_id", ""))) == (
PlayerArtUnlocks.STAMP_PRODUCT_ID
):
stamp_button = upgrade_button
break
assert(stamp_button != null)
assert(stamp_button.text == "stamp")
assert(stamp_button.icon == null)
var price_bubbles := shop.find_children(
"PriceBubble", "PanelContainer", true, false
)

View file

@ -65,6 +65,7 @@ func _run_host() -> void:
assert(players_page != null)
players_page.call("_refresh")
assert(_has_button_text(players_page, "deop"))
assert(_has_button_text(players_page, "clear art"))
var unban_deadline: int = Time.get_ticks_msec() + 12000
while (

View file

@ -2,7 +2,7 @@ extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18133
const WAIT_MSEC: int = 20000
const WAIT_MSEC: int = 60000
func _initialize() -> void:
@ -83,6 +83,10 @@ func _run_host() -> void:
str(overwritten_cell["author_fingerprint"])
== remote_record.identity_fingerprint
)
assert(
remote_record.identity_fingerprint
in overwritten_state.get("participant_fingerprints", [])
)
assert(service.request_cell_edits(canvas_id, [{
"x": 8,
@ -112,7 +116,61 @@ func _run_host() -> void:
assert(service.request_guide_visibility(canvas_id, false, true))
await create_timer(1.0).timeout
assert(service.clear_session_artwork())
var stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < stamp_deadline
and service.get_canvas_ids().is_empty()
):
await process_frame
assert(service.get_canvas_ids().size() == 1)
var stamp_id: String = service.get_canvas_ids()[0]
var stamp_state: Dictionary = service.get_canvas_state(stamp_id)
assert(bool(stamp_state.get("stamp", false)))
assert(bool(stamp_state["finalized"]))
assert((stamp_state["cells"] as Array).size() == 1)
assert(str(stamp_state["cells"][0]["color_id"]) == "coral")
assert(
stamp_state.get("participant_fingerprints", [])
== [remote_record.identity_fingerprint]
)
# Give the client a full observation window before the authoritative clear.
await create_timer(1.0).timeout
assert(
service.clear_artwork_by_fingerprint(
remote_record.identity_fingerprint
) == 1
)
assert(service.get_canvas_ids().is_empty())
var second_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < second_stamp_deadline
and service.get_canvas_ids().is_empty()
):
await process_frame
assert(service.get_canvas_ids().size() == 1)
var player_list := main.get_node(
"%NetworkPlayerListService"
) as NetworkPlayerListService
var remote_entry: PlayerListEntry = null
for entry: PlayerListEntry in player_list.get_entries():
if entry.peer_id == remote_peer_id:
remote_entry = entry
break
assert(remote_entry != null and remote_entry.can_ban)
assert(player_list.ban(
remote_peer_id,
remote_record.identity_fingerprint,
remote_record.display_name,
remote_entry.revision,
))
assert(service.get_canvas_ids().is_empty())
var removed_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < removed_deadline
and session.is_authenticated_peer(remote_peer_id)
):
await process_frame
assert(not session.is_authenticated_peer(remote_peer_id))
print("Surface drawing multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
@ -210,8 +268,63 @@ func _run_client() -> void:
if service.get_canvas_ids().is_empty():
break
assert(service.get_canvas_ids().is_empty())
var data_root := main.get("_data_root") as PlayerDataRoot
var stamp_directory: String = data_root.root_path.path_join("artwork")
assert(DirAccess.make_dir_recursive_absolute(stamp_directory) == OK)
var stamp_path: String = stamp_directory.path_join("multiplayer-stamp.png")
var stamp_image := Image.create(16, 16, false, Image.FORMAT_RGBA8)
stamp_image.fill(Color.TRANSPARENT)
stamp_image.set_pixel(3, 11, SurfaceDrawingPalette.get_color(&"coral"))
assert(stamp_image.save_png(stamp_path) == OK)
assert(service.select_saved_stamp(stamp_path))
var stamp_surface: Dictionary = _surface_below_local_player(main)
assert(not stamp_surface.is_empty())
assert(service.request_stamp_at_surface(
stamp_surface["position"], stamp_surface["normal"], Vector3.RIGHT
))
var shared_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < shared_stamp_deadline
and service.get_canvas_ids().is_empty()
):
await process_frame
assert(service.get_canvas_ids().size() == 1)
var stamp_state: Dictionary = service.get_canvas_state(
service.get_canvas_ids()[0]
)
assert(bool(stamp_state.get("stamp", false)))
assert(bool(stamp_state["finalized"]))
assert(str(stamp_state["cells"][0]["color_id"]) == "coral")
assert(
session.get_local_identity_fingerprint()
in stamp_state.get("participant_fingerprints", [])
)
var clear_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < clear_deadline
and not service.get_canvas_ids().is_empty()
):
await process_frame
assert(service.get_canvas_ids().is_empty())
assert(service.request_stamp_at_surface(
stamp_surface["position"], stamp_surface["normal"], Vector3.RIGHT
))
var second_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < second_stamp_deadline
and service.get_canvas_ids().is_empty()
):
await process_frame
assert(service.get_canvas_ids().size() == 1)
var ban_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
while (
Time.get_ticks_msec() < ban_deadline
and session.state != NetworkSession.State.SERVER_LOST
):
await process_frame
assert(session.state == NetworkSession.State.SERVER_LOST)
assert(service.get_canvas_ids().is_empty())
print("Surface drawing multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
for _frame: int in 4:
await process_frame

View file

@ -67,6 +67,16 @@ func _run() -> void:
state["cells"][0]["author_fingerprint"]
== session.get_local_identity_fingerprint()
)
var export_path: String = service.export_canvas_png(canvas_id)
assert(not export_path.is_empty())
var data_root := main.get("_data_root") as PlayerDataRoot
assert(export_path.begins_with(data_root.root_path.path_join("artwork")))
assert(FileAccess.file_exists(export_path))
var exported_image: Image = Image.load_from_file(export_path)
assert(exported_image.get_size() == Vector2i(16, 16))
assert(exported_image.get_pixel(8, 7).is_equal_approx(
SurfaceDrawingPalette.get_color(&"ocean_teal")
))
assert(service.request_guide_visibility(canvas_id, false))
state = service.get_canvas_state(canvas_id)
assert(not bool(state["guide_visible"]))
@ -78,6 +88,7 @@ func _run() -> void:
assert(service.request_guide_visibility(canvas_id, false, true))
assert(bool(service.get_canvas_state(canvas_id)["finalized"]))
assert(canvas.is_finalized())
assert(not canvas.has_guide_geometry())
assert(service.request_canvas_at_surface(
hit["position"], hit["normal"], Vector3.RIGHT
))
@ -109,6 +120,38 @@ func _run() -> void:
assert(player_list.get_session_artwork_counts() == Vector2i(2, 1))
assert(player_list.reset_session_artwork())
assert(service.get_canvas_ids().is_empty())
var stamp_entries: Array[Dictionary] = service.get_saved_stamp_entries()
assert(stamp_entries.size() == 1)
assert(str(stamp_entries[0]["path"]) == export_path)
assert(int(stamp_entries[0]["grid_size"]) == 16)
assert(bool(stamp_entries[0]["available"]))
service.activate(Vector2(640.0, 360.0))
assert(service.select_saved_stamp(export_path))
assert(service.is_stamp_mode())
assert(service.request_stamp_at_surface(
hit["position"], hit["normal"], Vector3.RIGHT
))
assert(service.get_canvas_ids().size() == 1)
var stamp_id: String = service.get_canvas_ids()[0]
var stamp_state: Dictionary = service.get_canvas_state(stamp_id)
assert(bool(stamp_state["stamp"]))
assert(bool(stamp_state["finalized"]))
assert(not bool(stamp_state["guide_visible"]))
assert((stamp_state["cells"] as Array).size() == 1)
assert(int(stamp_state["cells"][0]["x"]) == 8)
assert(int(stamp_state["cells"][0]["y"]) == 8)
assert(str(stamp_state["cells"][0]["color_id"]) == "ocean_teal")
assert(
stamp_state.get("participant_fingerprints", [])
== [session.get_local_identity_fingerprint()]
)
var stamp_canvas_nodes: Dictionary = service.get("_canvas_nodes")
var stamp_canvas := stamp_canvas_nodes[stamp_id] as SurfaceDrawingCanvas
assert(stamp_canvas.is_finalized())
assert(not stamp_canvas.has_guide_geometry())
service.set_placement_mode(false)
assert(not service.is_stamp_mode())
assert(service.clear_session_artwork())
print("Surface drawing runtime validation: PASS")
session.disconnect_session("")

View file

@ -0,0 +1,101 @@
extends SceneTree
const CANVAS_COUNT: int = 5
const GRID_SIZE: int = 32
const FINGERPRINT: String = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var world := Node3D.new()
root.add_child(world)
var color_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
var cells: Array[Dictionary] = []
for y: int in range(GRID_SIZE):
for x: int in range(GRID_SIZE):
cells.append({
"x": x,
"y": y,
"color_id": str(color_ids[(x + y) % color_ids.size()]),
"author_fingerprint": FINGERPRINT,
})
var canvases: Array[SurfaceDrawingCanvas] = []
var persistent_meshes: Array[Mesh] = []
var persistent_textures: Array[Texture2D] = []
for canvas_index: int in range(CANVAS_COUNT):
var canvas := SurfaceDrawingCanvas.new()
world.add_child(canvas)
assert(canvas.setup({
"session_id": "texture-renderer-session",
"canvas_id": "canvas-%d" % canvas_index,
"origin": [float(canvas_index) * 3.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": 0,
"guide_visible": true,
"finalized": false,
"layer": canvas_index,
"creator_fingerprint": FINGERPRINT,
"cells": cells,
}, null))
assert(canvas.get_rendered_pixel_count() == GRID_SIZE * GRID_SIZE)
var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
assert(pixel_instance != null)
assert(pixel_instance.mesh.get_surface_count() == 1)
var material := pixel_instance.mesh.surface_get_material(
0
) as StandardMaterial3D
assert(material != null)
assert(material.albedo_texture is ImageTexture)
canvases.append(canvas)
persistent_meshes.append(pixel_instance.mesh)
persistent_textures.append(material.albedo_texture)
for canvas_index: int in range(CANVAS_COUNT):
var canvas: SurfaceDrawingCanvas = canvases[canvas_index]
assert(canvas.apply_update({
"session_id": "texture-renderer-session",
"canvas_id": "canvas-%d" % canvas_index,
"revision": 1,
"edits": [{
"x": canvas_index,
"y": canvas_index,
"color_id": "coral",
"author_fingerprint": FINGERPRINT,
}],
}))
var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
var material := pixel_instance.mesh.surface_get_material(
0
) as StandardMaterial3D
assert(pixel_instance.mesh == persistent_meshes[canvas_index])
assert(material.albedo_texture == persistent_textures[canvas_index])
assert(canvas.apply_guide_update({
"session_id": "texture-renderer-session",
"canvas_id": "canvas-%d" % canvas_index,
"revision": 2,
"guide_visible": false,
"finalized": true,
}))
assert(not canvas.has_guide_geometry())
await process_frame
for canvas: SurfaceDrawingCanvas in canvases:
var artwork_mesh_count: int = 0
for child: Node in canvas.get_children():
if child is MultiMeshInstance3D:
assert(false, "Art Kit pixels regressed to per-color MultiMeshes.")
if child is MeshInstance3D:
artwork_mesh_count += 1
assert(artwork_mesh_count == 1)
assert(canvas.get_export_image().get_size() == Vector2i(32, 32))
world.queue_free()
await process_frame
print("Surface drawing texture renderer validation: PASS")
quit()

View file

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

View file

@ -90,6 +90,21 @@ func _validate_protocol_bounds() -> void:
unsupported_size["width"] = 48
unsupported_size["height"] = 48
assert(not SurfaceDrawingProtocol.validate_canvas_request(unsupported_size))
var stamp_pixels := PackedByteArray()
stamp_pixels.resize(16 * 16)
stamp_pixels[0] = 1
var stamp_request: Dictionary = canvas_request.duplicate(true)
stamp_request["request_id"] = "stamp-1"
stamp_request["pixels"] = stamp_pixels
assert(SurfaceDrawingProtocol.validate_stamp_request(stamp_request))
var invalid_stamp: Dictionary = stamp_request.duplicate(true)
var invalid_pixels: PackedByteArray = invalid_stamp["pixels"]
invalid_pixels[0] = SurfaceDrawingPalette.COLORS.size() + 1
invalid_stamp["pixels"] = invalid_pixels
assert(not SurfaceDrawingProtocol.validate_stamp_request(invalid_stamp))
var empty_stamp: Dictionary = stamp_request.duplicate(true)
empty_stamp["pixels"] = PackedByteArray()
assert(not SurfaceDrawingProtocol.validate_stamp_request(empty_stamp))
var guide_request: Dictionary = {
"request_id": "guide-1",
"session_id": "session",
@ -116,9 +131,11 @@ func _validate_art_unlocks() -> void:
assert(unlocks.unlock_product(&"marker_ocean_teal"))
assert(unlocks.unlock_product(&"brush_4x"))
assert(unlocks.unlock_product(&"grid_128x"))
assert(unlocks.unlock_product(PlayerArtUnlocks.STAMP_PRODUCT_ID))
assert(unlocks.is_color_unlocked(&"ocean_teal"))
assert(unlocks.is_brush_size_unlocked(4))
assert(unlocks.is_grid_size_unlocked(128))
assert(unlocks.is_stamp_unlocked())
assert(not unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK + 1))
unlocks.free()
@ -177,6 +194,16 @@ func _validate_canvas_geometry_and_collaboration() -> void:
"cells": [],
}
assert(canvas.setup(state, null))
var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
assert(pixel_instance != null)
assert(pixel_instance.name == "ArtworkTexture")
var persistent_mesh: Mesh = pixel_instance.mesh
var pixel_material := persistent_mesh.surface_get_material(
0
) as StandardMaterial3D
assert(pixel_material != null)
var persistent_texture: Texture2D = pixel_material.albedo_texture
assert(persistent_texture is ImageTexture)
var first_center: Vector3 = canvas.get_cell_world_position(0, 0)
assert(canvas.cell_at_world_point(first_center) == Vector2i(0, 0))
assert(canvas.contains_world_point(first_center))
@ -192,6 +219,15 @@ func _validate_canvas_geometry_and_collaboration() -> void:
}],
}
assert(canvas.apply_update(first_update))
assert(pixel_instance.mesh == persistent_mesh)
assert(pixel_material.albedo_texture == persistent_texture)
assert(canvas.get_rendered_pixel_count() == 1)
var export_image: Image = canvas.get_export_image()
assert(export_image.get_size() == Vector2i(16, 16))
assert(export_image.get_pixel(3, 11).is_equal_approx(
SurfaceDrawingPalette.get_color(&"coral")
))
assert(export_image.get_pixel(0, 0).is_equal_approx(Color.TRANSPARENT))
var finish_update: Dictionary = {
"session_id": "session",
"canvas_id": "canvas-1",
@ -202,6 +238,9 @@ func _validate_canvas_geometry_and_collaboration() -> void:
assert(canvas.apply_guide_update(finish_update))
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)
await process_frame
var finished_pixel_scale: float = canvas.get_rendered_pixel_size()
assert(
@ -223,6 +262,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)
var cells: Array[Dictionary] = canvas.get_authoritative_cells()
assert(cells.size() == 1)
assert(cells[0]["color_id"] == "blue")
@ -240,6 +281,10 @@ func _validate_canvas_geometry_and_collaboration() -> void:
}
assert(canvas.apply_update(erase_update))
assert(canvas.get_authoritative_cells().is_empty())
assert(canvas.get_rendered_pixel_count() == 0)
assert(canvas.get_export_image().get_pixel(3, 11).is_equal_approx(
Color.TRANSPARENT
))
world.queue_free()
await process_frame
@ -282,6 +327,7 @@ func _validate_blocked_author_visibility() -> void:
"cell_size": SurfaceDrawingProtocol.CELL_SIZE,
"revision": 1,
"creator_fingerprint": FINGERPRINT_A,
"participant_fingerprints": [FINGERPRINT_A, FINGERPRINT_B],
"cells": [
{
"x": 1,
@ -298,20 +344,30 @@ func _validate_blocked_author_visibility() -> void:
],
}
assert(canvas.setup(state, relationships))
assert(_rendered_pixel_count(canvas) == 1)
assert(canvas.is_hidden_by_relationship())
assert(canvas.get_rendered_pixel_count() == 0)
relationships.set("_records", {})
canvas.refresh_relationship_visibility()
await process_frame
assert(_rendered_pixel_count(canvas) == 2)
assert(not canvas.is_hidden_by_relationship())
assert(canvas.get_rendered_pixel_count() == 2)
relationships.set("_records", {
FINGERPRINT_B: {"blocked": true, "muted": true},
})
assert(canvas.apply_update({
"session_id": "session",
"canvas_id": "blocked-author-canvas",
"revision": 2,
"participant_fingerprints": [FINGERPRINT_A, FINGERPRINT_B],
"edits": [{
"x": 2,
"y": 1,
"color_id": "",
"author_fingerprint": "",
}],
}))
canvas.refresh_relationship_visibility()
assert(canvas.is_hidden_by_relationship())
assert(canvas.get_rendered_pixel_count() == 0)
world.queue_free()
await process_frame
func _rendered_pixel_count(canvas: SurfaceDrawingCanvas) -> int:
var count: int = 0
for child: Node in canvas.get_children():
if child is MultiMeshInstance3D:
var instance := child as MultiMeshInstance3D
if instance.multimesh != null:
count += instance.multimesh.instance_count
return count