Add collaborative surface drawing
This commit is contained in:
parent
6b7ef4c11b
commit
b92c55562d
28 changed files with 3504 additions and 2 deletions
393
drawing/surface_drawing_canvas.gd
Normal file
393
drawing/surface_drawing_canvas.gd
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
class_name SurfaceDrawingCanvas
|
||||
extends Node3D
|
||||
|
||||
const GUIDED_PIXEL_FILL: float = 0.88
|
||||
const FINISHED_PIXEL_FILL: float = 1.0
|
||||
const SURFACE_OFFSET: float = 0.012
|
||||
const LAYER_OFFSET_STEP: float = 0.0002
|
||||
const SURFACE_SAMPLE_DEPTH: float = 0.45
|
||||
|
||||
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 _surface_origin: Vector3
|
||||
var _surface_normal: Vector3
|
||||
var _surface_tangent: Vector3
|
||||
var _surface_bitangent: Vector3
|
||||
var _cells: Dictionary[int, Dictionary] = {}
|
||||
var _color_meshes: Dictionary[StringName, MultiMeshInstance3D] = {}
|
||||
var _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,
|
||||
) -> bool:
|
||||
if not SurfaceDrawingProtocol.validate_canvas_state(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
|
||||
_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()
|
||||
_rebuild_pixels()
|
||||
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
|
||||
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)
|
||||
revision = int(data["revision"])
|
||||
_rebuild_pixels()
|
||||
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"])
|
||||
_finalized = bool(data["finalized"])
|
||||
_guide_visible = bool(data["guide_visible"]) and not _finalized
|
||||
_refresh_grid_visibility()
|
||||
_rebuild_pixels()
|
||||
return true
|
||||
|
||||
|
||||
func refresh_relationship_visibility() -> void:
|
||||
_rebuild_pixels()
|
||||
|
||||
|
||||
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 * _pixel_fill()
|
||||
|
||||
|
||||
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
|
||||
return global_transform * _sample_cell_transform(x, y, fill)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
func _build_grid() -> void:
|
||||
if _grid_instance != null:
|
||||
_grid_instance.queue_free()
|
||||
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)
|
||||
var half_width: float = float(grid_width) * cell_size * 0.5
|
||||
var half_height: float = float(grid_height) * cell_size * 0.5
|
||||
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,
|
||||
)
|
||||
_add_grid_vertex(
|
||||
immediate,
|
||||
_surface_origin + _surface_tangent * horizontal
|
||||
+ _surface_bitangent * (start_vertical + cell_size),
|
||||
)
|
||||
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,
|
||||
)
|
||||
_add_grid_vertex(
|
||||
immediate,
|
||||
_surface_origin
|
||||
+ _surface_tangent * (start_horizontal + cell_size)
|
||||
+ _surface_bitangent * vertical,
|
||||
)
|
||||
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, world_point: Vector3) -> void:
|
||||
var sampled: Dictionary = _sample_surface(world_point)
|
||||
var point: Vector3 = sampled["position"]
|
||||
var normal: Vector3 = sampled["normal"]
|
||||
immediate.surface_add_vertex(
|
||||
to_local(point + normal * (_surface_offset() * 1.5))
|
||||
)
|
||||
|
||||
|
||||
func _rebuild_pixels() -> void:
|
||||
for instance: MultiMeshInstance3D in _color_meshes.values():
|
||||
instance.queue_free()
|
||||
_color_meshes.clear()
|
||||
var cells_by_color: Dictionary[StringName, Array] = {}
|
||||
for cell: Dictionary in _cells.values():
|
||||
var author_fingerprint: String = str(cell.get("author_fingerprint", ""))
|
||||
if (
|
||||
_relationships != null
|
||||
and _relationships.is_blocked(author_fingerprint)
|
||||
):
|
||||
continue
|
||||
var color_id := StringName(str(cell.get("color_id", "")))
|
||||
if not SurfaceDrawingPalette.has_color(color_id):
|
||||
continue
|
||||
var color_cells: Array = cells_by_color.get(color_id, [])
|
||||
color_cells.append(cell)
|
||||
cells_by_color[color_id] = color_cells
|
||||
for color_id: StringName in cells_by_color:
|
||||
_create_color_mesh(color_id, cells_by_color[color_id])
|
||||
|
||||
|
||||
func _create_color_mesh(color_id: StringName, cells: Array) -> void:
|
||||
if cells.is_empty():
|
||||
return
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2.ONE
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.albedo_color = SurfaceDrawingPalette.get_color(color_id)
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
quad.material = material
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.mesh = quad
|
||||
multimesh.instance_count = cells.size()
|
||||
for cell_index: int in range(cells.size()):
|
||||
var cell: Dictionary = cells[cell_index]
|
||||
multimesh.set_instance_transform(
|
||||
cell_index,
|
||||
_sample_cell_transform(
|
||||
int(cell["x"]), int(cell["y"]), _pixel_fill()
|
||||
),
|
||||
)
|
||||
var instance := MultiMeshInstance3D.new()
|
||||
instance.name = "Pixels_%s" % color_id
|
||||
instance.multimesh = multimesh
|
||||
instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(instance)
|
||||
_color_meshes[color_id] = instance
|
||||
|
||||
|
||||
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 basis := Basis(
|
||||
tangent * pixel_size,
|
||||
bitangent * pixel_size,
|
||||
normal,
|
||||
)
|
||||
return Transform3D(
|
||||
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 _pixel_fill() -> float:
|
||||
return GUIDED_PIXEL_FILL if _guide_visible else FINISHED_PIXEL_FILL
|
||||
|
||||
|
||||
func _surface_offset() -> float:
|
||||
return SURFACE_OFFSET + float(_layer) * LAYER_OFFSET_STEP
|
||||
|
||||
|
||||
func _refresh_grid_visibility() -> void:
|
||||
if _grid_instance != null:
|
||||
_grid_instance.visible = _stencil_requested_visible and _guide_visible
|
||||
|
||||
|
||||
func _cell_key(x: int, y: int) -> int:
|
||||
return y * grid_width + x
|
||||
1
drawing/surface_drawing_canvas.gd.uid
Normal file
1
drawing/surface_drawing_canvas.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cj2gq63l4mysl
|
||||
12
drawing/surface_drawing_highlight.gdshader
Normal file
12
drawing/surface_drawing_highlight.gdshader
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
shader_type spatial;
|
||||
render_mode unshaded, cull_disabled, depth_draw_never;
|
||||
|
||||
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec3 surface_color = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
|
||||
vec3 inverted_color = vec3(1.0) - surface_color;
|
||||
ALBEDO = inverted_color;
|
||||
EMISSION = inverted_color;
|
||||
}
|
||||
1
drawing/surface_drawing_highlight.gdshader.uid
Normal file
1
drawing/surface_drawing_highlight.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://k82me23ylsos
|
||||
57
drawing/surface_drawing_palette.gd
Normal file
57
drawing/surface_drawing_palette.gd
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
class_name SurfaceDrawingPalette
|
||||
extends RefCounted
|
||||
|
||||
const DEFAULT_COLOR_ID: StringName = &"chalk_white"
|
||||
|
||||
# Stable IDs are the network and future progression boundary. New colors may
|
||||
# be appended without changing drawings created with an older palette.
|
||||
const COLORS: Array[Dictionary] = [
|
||||
{"id": &"chalk_white", "name": "Chalk white", "color": Color("f5eed9")},
|
||||
{"id": &"ocean_teal", "name": "Ocean teal", "color": Color("35b9c7")},
|
||||
{"id": &"coral", "name": "Coral", "color": Color("ef5b62")},
|
||||
{"id": &"sunny", "name": "Sunny", "color": Color("ffd166")},
|
||||
{"id": &"leaf", "name": "Leaf", "color": Color("46c878")},
|
||||
{"id": &"blue", "name": "Blue", "color": Color("5596f6")},
|
||||
{"id": &"violet", "name": "Violet", "color": Color("b176e8")},
|
||||
{"id": &"charcoal", "name": "Charcoal", "color": Color("28251f")},
|
||||
]
|
||||
|
||||
|
||||
static func has_color(color_id: StringName) -> bool:
|
||||
return not get_entry(color_id).is_empty()
|
||||
|
||||
|
||||
static func get_entry(color_id: StringName) -> Dictionary:
|
||||
for entry: Dictionary in COLORS:
|
||||
if StringName(entry.get("id", &"")) == color_id:
|
||||
return entry
|
||||
return {}
|
||||
|
||||
|
||||
static func get_color(color_id: StringName) -> Color:
|
||||
var entry: Dictionary = get_entry(color_id)
|
||||
var value: Variant = entry.get("color", Color.WHITE)
|
||||
return value if typeof(value) == TYPE_COLOR else Color.WHITE
|
||||
|
||||
|
||||
static func get_display_name(color_id: StringName) -> String:
|
||||
return str(get_entry(color_id).get("name", "Unknown"))
|
||||
|
||||
|
||||
static func get_color_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for entry: Dictionary in COLORS:
|
||||
result.append(StringName(entry.get("id", &"")))
|
||||
return result
|
||||
|
||||
|
||||
static func filter_unlocked_ids(
|
||||
unlocked_ids: Array[StringName],
|
||||
) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for color_id: StringName in unlocked_ids:
|
||||
if has_color(color_id) and color_id not in result:
|
||||
result.append(color_id)
|
||||
if result.is_empty():
|
||||
result.append(DEFAULT_COLOR_ID)
|
||||
return result
|
||||
1
drawing/surface_drawing_palette.gd.uid
Normal file
1
drawing/surface_drawing_palette.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bk16cl6f3v887
|
||||
82
drawing/surface_drawing_placement.gd
Normal file
82
drawing/surface_drawing_placement.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
class_name SurfaceDrawingPlacement
|
||||
extends RefCounted
|
||||
|
||||
const SNAP_DISTANCE: float = 0.55
|
||||
const SNAP_PLANE_DISTANCE: float = 0.18
|
||||
const SNAP_NORMAL_DOT: float = 0.94
|
||||
|
||||
|
||||
static func resolve(
|
||||
origin: Vector3,
|
||||
normal: Vector3,
|
||||
fallback_tangent: Vector3,
|
||||
canvas_states: Array[Dictionary],
|
||||
) -> Dictionary:
|
||||
var surface_normal: Vector3 = normal.normalized()
|
||||
var surface_tangent: Vector3 = _projected_tangent(
|
||||
fallback_tangent, surface_normal
|
||||
)
|
||||
var best: Dictionary = {
|
||||
"origin": origin,
|
||||
"normal": surface_normal,
|
||||
"tangent": surface_tangent,
|
||||
"snapped": false,
|
||||
}
|
||||
var nearest_distance: float = SNAP_DISTANCE
|
||||
for state: Dictionary in canvas_states:
|
||||
if not SurfaceDrawingProtocol.validate_canvas_state(state):
|
||||
continue
|
||||
var anchor_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["normal"]
|
||||
).normalized()
|
||||
if anchor_normal.dot(surface_normal) < SNAP_NORMAL_DOT:
|
||||
continue
|
||||
var anchor_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
state["origin"]
|
||||
)
|
||||
var relative: Vector3 = origin - anchor_origin
|
||||
if absf(relative.dot(anchor_normal)) > SNAP_PLANE_DISTANCE:
|
||||
continue
|
||||
var anchor_tangent: Vector3 = _projected_tangent(
|
||||
SurfaceDrawingProtocol.array_to_vector(state["tangent"]),
|
||||
anchor_normal,
|
||||
)
|
||||
var anchor_bitangent: Vector3 = anchor_normal.cross(
|
||||
anchor_tangent
|
||||
).normalized()
|
||||
var width: float = float(state["width"]) * float(state["cell_size"])
|
||||
var height: float = float(state["height"]) * float(state["cell_size"])
|
||||
if width <= 0.0 or height <= 0.0:
|
||||
continue
|
||||
var horizontal_step: int = roundi(relative.dot(anchor_tangent) / width)
|
||||
var vertical_step: int = roundi(relative.dot(anchor_bitangent) / height)
|
||||
if horizontal_step == 0 and vertical_step == 0:
|
||||
continue
|
||||
var candidate: Vector3 = (
|
||||
anchor_origin
|
||||
+ anchor_tangent * float(horizontal_step) * width
|
||||
+ anchor_bitangent * float(vertical_step) * height
|
||||
)
|
||||
var distance: float = candidate.distance_to(origin)
|
||||
if distance > nearest_distance:
|
||||
continue
|
||||
nearest_distance = distance
|
||||
best = {
|
||||
"origin": candidate,
|
||||
"normal": anchor_normal,
|
||||
"tangent": anchor_tangent,
|
||||
"snapped": true,
|
||||
}
|
||||
return best
|
||||
|
||||
|
||||
static func _projected_tangent(
|
||||
value: Vector3,
|
||||
normal: Vector3,
|
||||
) -> Vector3:
|
||||
var tangent: Vector3 = (value - normal * value.dot(normal)).normalized()
|
||||
if tangent.is_zero_approx():
|
||||
tangent = Vector3.UP.cross(normal).normalized()
|
||||
if tangent.is_zero_approx():
|
||||
tangent = Vector3.RIGHT
|
||||
return tangent
|
||||
1
drawing/surface_drawing_placement.gd.uid
Normal file
1
drawing/surface_drawing_placement.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgb7stq5k85d0
|
||||
243
drawing/surface_drawing_protocol.gd
Normal file
243
drawing/surface_drawing_protocol.gd
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
class_name SurfaceDrawingProtocol
|
||||
extends RefCounted
|
||||
|
||||
const CAPABILITY: StringName = &"surface_drawing_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
|
||||
const GRID_WIDTH: int = 32
|
||||
const GRID_HEIGHT: int = 32
|
||||
const CELL_SIZE: float = 0.075
|
||||
const MAX_ACTIVE_CANVASES: int = 24
|
||||
const MAX_CANVASES: int = 48
|
||||
const MAX_EDITS_PER_REQUEST: int = 16
|
||||
const MAX_CANVAS_ID_LENGTH: int = 64
|
||||
const MAX_REQUEST_ID_LENGTH: int = 64
|
||||
const MAX_STROKE_ID_LENGTH: int = 64
|
||||
const MAX_SESSION_ID_LENGTH: int = 96
|
||||
const MAX_COORDINATE: float = 10000.0
|
||||
|
||||
|
||||
static func validate_canvas_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
_valid_common(value)
|
||||
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"]) == GRID_WIDTH
|
||||
and typeof(value.get("height")) == TYPE_INT
|
||||
and int(value["height"]) == GRID_HEIGHT
|
||||
and typeof(value.get("cell_size")) in [TYPE_FLOAT, TYPE_INT]
|
||||
and is_equal_approx(float(value["cell_size"]), CELL_SIZE)
|
||||
)
|
||||
|
||||
|
||||
static func validate_edit_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
if (
|
||||
not _valid_common(value)
|
||||
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_stroke_id(value.get("stroke_id"))
|
||||
or typeof(value.get("edits")) != TYPE_ARRAY
|
||||
):
|
||||
return false
|
||||
var edits: Array = value["edits"]
|
||||
if edits.is_empty() or edits.size() > MAX_EDITS_PER_REQUEST:
|
||||
return false
|
||||
for edit_value: Variant in edits:
|
||||
if not validate_cell_edit(edit_value):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func validate_guide_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
_valid_common(value)
|
||||
and _valid_canvas_id(value.get("canvas_id"))
|
||||
and typeof(value.get("guide_visible")) == TYPE_BOOL
|
||||
and typeof(value.get("finalized")) == TYPE_BOOL
|
||||
)
|
||||
|
||||
|
||||
static func validate_undo_request(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return _valid_common(value) and _valid_stroke_id(value.get("stroke_id"))
|
||||
|
||||
|
||||
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"]) != GRID_WIDTH
|
||||
or typeof(value.get("height")) != TYPE_INT
|
||||
or int(value["height"]) != GRID_HEIGHT
|
||||
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 typeof(value.get("cells")) != TYPE_ARRAY
|
||||
):
|
||||
return false
|
||||
var cells: Array = value["cells"]
|
||||
if cells.size() > GRID_WIDTH * GRID_HEIGHT:
|
||||
return false
|
||||
for cell_value: Variant in cells:
|
||||
if not validate_authoritative_cell(cell_value):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func validate_guide_update(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 _valid_canvas_id(value.get("canvas_id"))
|
||||
and typeof(value.get("revision")) == TYPE_INT
|
||||
and int(value["revision"]) >= 1
|
||||
and typeof(value.get("guide_visible")) == TYPE_BOOL
|
||||
and typeof(value.get("finalized")) == TYPE_BOOL
|
||||
)
|
||||
|
||||
|
||||
static func validate_canvas_update(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 typeof(value.get("revision")) != TYPE_INT
|
||||
or int(value["revision"]) < 1
|
||||
or typeof(value.get("edits")) != TYPE_ARRAY
|
||||
):
|
||||
return false
|
||||
var edits: Array = value["edits"]
|
||||
if edits.is_empty() or edits.size() > MAX_EDITS_PER_REQUEST:
|
||||
return false
|
||||
for edit_value: Variant in edits:
|
||||
if not validate_authoritative_cell(edit_value, true):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func validate_cell_edit(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
if (
|
||||
typeof(value.get("x")) != TYPE_INT
|
||||
or int(value["x"]) < 0
|
||||
or int(value["x"]) >= GRID_WIDTH
|
||||
or typeof(value.get("y")) != TYPE_INT
|
||||
or int(value["y"]) < 0
|
||||
or int(value["y"]) >= GRID_HEIGHT
|
||||
or typeof(value.get("color_id")) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
):
|
||||
return false
|
||||
var color_id := StringName(str(value["color_id"]))
|
||||
return color_id.is_empty() or SurfaceDrawingPalette.has_color(color_id)
|
||||
|
||||
|
||||
static func validate_authoritative_cell(
|
||||
data: Variant,
|
||||
allow_erased: bool = false,
|
||||
) -> bool:
|
||||
if not validate_cell_edit(data):
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
var color_id := StringName(str(value["color_id"]))
|
||||
if color_id.is_empty():
|
||||
return allow_erased and str(value.get("author_fingerprint", "")).is_empty()
|
||||
return NetworkIdentityCrypto.valid_fingerprint(
|
||||
value.get("author_fingerprint")
|
||||
)
|
||||
|
||||
|
||||
static func vector_to_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
static func array_to_vector(value: Variant) -> Vector3:
|
||||
if not _valid_vector(value):
|
||||
return Vector3.ZERO
|
||||
var array: Array = value
|
||||
return Vector3(float(array[0]), float(array[1]), float(array[2]))
|
||||
|
||||
|
||||
static func _valid_common(value: Dictionary) -> bool:
|
||||
return (
|
||||
typeof(value.get("request_id")) == TYPE_STRING
|
||||
and not str(value["request_id"]).is_empty()
|
||||
and str(value["request_id"]).length() <= MAX_REQUEST_ID_LENGTH
|
||||
and 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
|
||||
)
|
||||
|
||||
|
||||
static func _valid_canvas_id(value: Variant) -> bool:
|
||||
return (
|
||||
typeof(value) == TYPE_STRING
|
||||
and not str(value).is_empty()
|
||||
and str(value).length() <= MAX_CANVAS_ID_LENGTH
|
||||
)
|
||||
|
||||
|
||||
static func _valid_stroke_id(value: Variant) -> bool:
|
||||
return (
|
||||
typeof(value) == TYPE_STRING
|
||||
and not str(value).is_empty()
|
||||
and str(value).length() <= MAX_STROKE_ID_LENGTH
|
||||
)
|
||||
|
||||
|
||||
static func _valid_vector(value: Variant) -> bool:
|
||||
if typeof(value) != TYPE_ARRAY:
|
||||
return false
|
||||
var array: Array = value
|
||||
if array.size() != 3:
|
||||
return false
|
||||
for component: Variant in array:
|
||||
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
|
||||
return false
|
||||
var number: float = float(component)
|
||||
if not is_finite(number) or absf(number) > MAX_COORDINATE:
|
||||
return false
|
||||
return true
|
||||
1
drawing/surface_drawing_protocol.gd.uid
Normal file
1
drawing/surface_drawing_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c258sivd8k8nc
|
||||
Loading…
Add table
Add a link
Reference in a new issue