class_name SurfaceDrawingCanvas extends Node3D 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 var grid_height: int = 0 var cell_size: float = 0.0 var revision: int = -1 var creator_fingerprint: String = "" var participant_fingerprints: Array[String] = [] var _surface_origin: Vector3 var _surface_normal: Vector3 var _surface_tangent: Vector3 var _surface_bitangent: Vector3 var _cell_buffer: SurfaceDrawingCellBuffer var _cell_surface_transforms: Array[Transform3D] = [] var _pixel_image: Image var _pixel_texture: ImageTexture var _pixel_instance: MeshInstance3D var _grid_instance: MeshInstance3D var _guide_visible: bool = true var _finalized: bool = false var _layer: int = 0 var _stencil_requested_visible: bool = false var _relationships: PlayerRelationshipStore var _solid_surface_mask: int = 1 func setup( data: Dictionary, relationships: PlayerRelationshipStore, solid_surface_mask: int = 1, cell_buffer: SurfaceDrawingCellBuffer = null, ) -> bool: 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"]) grid_height = int(data["height"]) cell_size = float(data["cell_size"]) revision = int(data["revision"]) creator_fingerprint = str(data["creator_fingerprint"]) _finalized = bool(data.get("finalized", false)) _guide_visible = bool(data.get("guide_visible", true)) and not _finalized _layer = int(data.get("layer", 0)) _surface_origin = SurfaceDrawingProtocol.array_to_vector(data["origin"]) _surface_normal = SurfaceDrawingProtocol.array_to_vector( data["normal"] ).normalized() _surface_tangent = SurfaceDrawingProtocol.array_to_vector( data["tangent"] ) _surface_tangent = ( _surface_tangent - _surface_normal * _surface_tangent.dot(_surface_normal) ).normalized() if _surface_normal.is_zero_approx() or _surface_tangent.is_zero_approx(): return false _surface_bitangent = _surface_normal.cross(_surface_tangent).normalized() _relationships = relationships _solid_surface_mask = solid_surface_mask _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 func apply_update(data: Dictionary) -> bool: if ( not SurfaceDrawingProtocol.validate_canvas_update(data) or str(data["canvas_id"]) != canvas_id or int(data["revision"]) <= revision ): return false 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() else: _apply_pixel_edits(data["edits"]) _refresh_grid_visibility() return true func apply_guide_update(data: Dictionary) -> bool: if ( not SurfaceDrawingProtocol.validate_guide_update(data) or str(data["canvas_id"]) != canvas_id or int(data["revision"]) <= revision ): return false revision = int(data["revision"]) 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 func refresh_relationship_visibility() -> void: _rebuild_pixel_image() if _pixel_instance != null: _pixel_instance.visible = not is_hidden_by_relationship() _refresh_grid_visibility() func is_hidden_by_relationship() -> bool: if _relationships == null: return false for fingerprint: String in participant_fingerprints: if _relationships.is_blocked(fingerprint): return true return false func set_stencil_visible(should_be_visible: bool) -> void: _stencil_requested_visible = should_be_visible _refresh_grid_visibility() func is_guide_visible() -> bool: return _guide_visible func is_finalized() -> bool: return _finalized func get_rendered_pixel_size() -> float: return cell_size func get_rendered_pixel_count() -> int: if is_hidden_by_relationship(): return 0 return _cell_buffer.get_painted_count() if _cell_buffer != null else 0 func get_export_image() -> Image: if _pixel_image == null or _pixel_image.is_empty(): return null return _pixel_image.duplicate() as Image func has_guide_geometry() -> bool: return _grid_instance != null and is_instance_valid(_grid_instance) func contains_world_point(world_point: Vector3, tolerance: float = 0.3) -> bool: var relative: Vector3 = world_point - _surface_origin if absf(relative.dot(_surface_normal)) > tolerance: return false var half_width: float = float(grid_width) * cell_size * 0.5 var half_height: float = float(grid_height) * cell_size * 0.5 var horizontal: float = relative.dot(_surface_tangent) var vertical: float = relative.dot(_surface_bitangent) return ( horizontal >= -half_width and horizontal < half_width and vertical >= -half_height and vertical < half_height ) func get_surface_plane_distance(world_point: Vector3) -> float: return absf((world_point - _surface_origin).dot(_surface_normal)) func cell_at_world_point(world_point: Vector3) -> Vector2i: var relative: Vector3 = world_point - _surface_origin var half_width: float = float(grid_width) * cell_size * 0.5 var half_height: float = float(grid_height) * cell_size * 0.5 var x: int = floori( (relative.dot(_surface_tangent) + half_width) / cell_size ) var y: int = floori( (relative.dot(_surface_bitangent) + half_height) / cell_size ) if x < 0 or x >= grid_width or y < 0 or y >= grid_height: return Vector2i(-1, -1) return Vector2i(x, y) func get_cell_world_position(x: int, y: int) -> Vector3: var horizontal: float = ( (float(x) + 0.5 - float(grid_width) * 0.5) * cell_size ) var vertical: float = ( (float(y) + 0.5 - float(grid_height) * 0.5) * cell_size ) return ( _surface_origin + _surface_tangent * horizontal + _surface_bitangent * vertical ) func get_surface_normal() -> Vector3: return _surface_normal func get_surface_tangent() -> Vector3: return _surface_tangent func get_surface_bitangent() -> Vector3: return _surface_bitangent func get_cell_surface_transform( x: int, y: int, fill: float = 0.94, ) -> Transform3D: if x < 0 or x >= grid_width or y < 0 or y >= grid_height: return Transform3D.IDENTITY var key: int = _cell_key(x, y) var local_transform: Transform3D = ( _cell_surface_transforms[key] if key >= 0 and key < _cell_surface_transforms.size() else _sample_cell_transform(x, y, 1.0) ) var scaled_basis: Basis = local_transform.basis var resolved_fill: float = clampf(fill, 0.05, 1.0) scaled_basis.x *= resolved_fill scaled_basis.y *= resolved_fill local_transform.basis = scaled_basis return global_transform * local_transform func get_authoritative_cells() -> Array[Dictionary]: return _cell_buffer.to_cells() if _cell_buffer != null else [] func _build_grid(samples: Dictionary) -> void: if _grid_instance != null: _grid_instance.queue_free() _grid_instance = null if _finalized: return var immediate := ImmediateMesh.new() var material := StandardMaterial3D.new() material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA material.albedo_color = Color(0.75, 0.94, 0.96, 0.38) material.no_depth_test = false immediate.surface_begin(Mesh.PRIMITIVE_LINES, material) for x: int in range(grid_width + 1): for y_segment: int in range(grid_height): _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): for x_segment: int in range(grid_width): _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" _grid_instance.mesh = immediate _grid_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(_grid_instance) _refresh_grid_visibility() func _add_grid_vertex( immediate: ImmediateMesh, x: int, y: int, samples: Dictionary, ) -> void: immediate.surface_add_vertex( _sample_position(samples, x, y, _surface_offset() * 1.5) ) 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(vertex_count) texture_coordinates.resize(vertex_count) indices.resize(cell_count * 6) _cell_surface_transforms.clear() 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) 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 + 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 _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 _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 _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: _pixel_texture.update(_pixel_image) func _apply_pixel_edits(edits: Array) -> void: if _pixel_image == null or _pixel_texture == null: _rebuild_pixel_image() return for edit_value: Variant in edits: var edit: Dictionary = edit_value var x: int = int(edit["x"]) var y: int = int(edit["y"]) var cell: Dictionary = _cell_buffer.get_cell(x, y) _set_image_cell(x, y, cell) _pixel_texture.update(_pixel_image) func _set_image_cell(x: int, y: int, cell: Dictionary) -> void: if ( _pixel_image == null or x < 0 or x >= grid_width or y < 0 or y >= grid_height ): return _pixel_image.set_pixel(x, _image_y(y), _visible_cell_color(cell)) func _visible_cell_color(cell: Dictionary) -> Color: if cell.is_empty(): return Color.TRANSPARENT var color_id := StringName(str(cell.get("color_id", ""))) return ( SurfaceDrawingPalette.get_color(color_id) if SurfaceDrawingPalette.has_color(color_id) else Color.TRANSPARENT ) func _image_y(cell_y: int) -> int: return grid_height - 1 - cell_y func _sample_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, fill: float = GUIDED_PIXEL_FILL, ) -> Transform3D: var expected: Vector3 = get_cell_world_position(x, y) var sampled: Dictionary = _sample_surface(expected) var point: Vector3 = sampled["position"] var normal: Vector3 = sampled["normal"] var tangent: Vector3 = ( _surface_tangent - normal * _surface_tangent.dot(normal) ).normalized() if tangent.is_zero_approx(): tangent = normal.cross(Vector3.UP).normalized() if tangent.is_zero_approx(): tangent = Vector3.RIGHT var bitangent: Vector3 = normal.cross(tangent).normalized() var pixel_size: float = cell_size * clampf(fill, 0.05, 1.0) var cell_basis := Basis( tangent * pixel_size, bitangent * pixel_size, normal, ) return Transform3D( cell_basis, to_local(point + normal * _surface_offset()), ) func _sample_surface(expected: Vector3) -> Dictionary: var point: Vector3 = expected var normal: Vector3 = _surface_normal var world: World3D = get_world_3d() if world != null: var query := PhysicsRayQueryParameters3D.create( expected + _surface_normal * SURFACE_SAMPLE_DEPTH, expected - _surface_normal * SURFACE_SAMPLE_DEPTH, _solid_surface_mask, ) query.collide_with_areas = false query.collide_with_bodies = true var hit: Dictionary = world.direct_space_state.intersect_ray(query) if ( not hit.is_empty() and hit.get("collider") is StaticBody3D ): var hit_normal: Vector3 = hit.get("normal", _surface_normal) if hit_normal.dot(_surface_normal) >= 0.35: var hit_position: Vector3 = hit.get("position", expected) point = hit_position normal = hit_normal.normalized() return {"position": point, "normal": normal} func _surface_offset() -> float: return SURFACE_OFFSET + float(_layer) * LAYER_OFFSET_STEP func _destroy_grid() -> void: if _grid_instance == null: return _grid_instance.queue_free() _grid_instance = null func _refresh_grid_visibility() -> void: if _grid_instance != null: _grid_instance.visible = ( _stencil_requested_visible and _guide_visible and not is_hidden_by_relationship() ) 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", ""))) for value: Variant in data.get("participant_fingerprints", []): _append_participant(result, str(value)) for cell_value: Variant in data.get("cells", []): var cell: Dictionary = cell_value _append_participant( result, str(cell.get("author_fingerprint", "")) ) return result func _participants_from_update( data: Dictionary, fallback: Array[String], ) -> Array[String]: var result: Array[String] = fallback.duplicate() for value: Variant in data.get("participant_fingerprints", []): _append_participant(result, str(value)) for edit_value: Variant in data.get("edits", []): var edit: Dictionary = edit_value _append_participant( result, str(edit.get("author_fingerprint", "")) ) return result func _append_participant(result: Array[String], fingerprint: String) -> void: if ( NetworkIdentityCrypto.valid_fingerprint(fingerprint) and fingerprint not in result ): result.append(fingerprint) func _cell_key(x: int, y: int) -> int: return y * grid_width + x