Add production bathymetric sonar slice

This commit is contained in:
chindit committed 2026-10-02 15:52:11 +02:00
1 parent a18e82eb44
commit 7613daee84
28 files changed
+1825 -87

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
class_name SonarContact
extends RefCounted
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
var contact_id: StringName
var material: SonarMaterial.Value
var center_m: Vector3
var bounds: AABB
var maximum_intensity: float
var sample_count: int
func _init(
p_contact_id: StringName,
p_material: SonarMaterial.Value,
p_center_m: Vector3,
p_bounds: AABB,
p_maximum_intensity: float,
p_sample_count: int
) -> void:
contact_id = p_contact_id
material = p_material
center_m = p_center_m
bounds = p_bounds
maximum_intensity = p_maximum_intensity
sample_count = p_sample_count
func validation_error() -> String:
if contact_id.is_empty():
return "sonar contact id is empty"
if material == SonarMaterial.Value.UNKNOWN or not SonarMaterial.is_valid(material):
return "sonar contact material is invalid"
if sample_count <= 0:
return "sonar contact sample count must be positive"
if not is_finite(maximum_intensity) or maximum_intensity < 0.0 or maximum_intensity > 1.0:
return "sonar contact intensity must be between 0 and 1"
return ""
+12
View File
@@ -0,0 +1,12 @@
class_name SonarMaterial
extends RefCounted
enum Value {
UNKNOWN,
ROCK,
METAL,
}
static func is_valid(value: int) -> bool:
return value >= Value.UNKNOWN and value <= Value.METAL
+53
View File
@@ -0,0 +1,53 @@
class_name SonarSample
extends RefCounted
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
var position_m: Vector3
var normal: Vector3
var intensity: float
var elevation_m: float
var material: SonarMaterial.Value
var observed: bool
func _init(
p_position_m: Vector3 = Vector3.ZERO,
p_normal: Vector3 = Vector3.UP,
p_intensity: float = 0.0,
p_elevation_m: float = 0.0,
p_material: SonarMaterial.Value = SonarMaterial.Value.UNKNOWN,
p_observed: bool = false
) -> void:
position_m = p_position_m
normal = p_normal
intensity = p_intensity
elevation_m = p_elevation_m
material = p_material
observed = p_observed
static func unknown() -> SonarSample:
return new()
func validation_error() -> String:
if not observed:
if material != SonarMaterial.Value.UNKNOWN:
return "unobserved sonar sample has a material"
return ""
if not _vector_is_finite(position_m):
return "sonar sample position is not finite"
if not _vector_is_finite(normal) or normal.length_squared() < 0.25:
return "sonar sample normal is invalid"
if not is_finite(intensity) or intensity < 0.0 or intensity > 1.0:
return "sonar sample intensity must be between 0 and 1"
if not is_finite(elevation_m):
return "sonar sample elevation is not finite"
if material == SonarMaterial.Value.UNKNOWN or not SonarMaterial.is_valid(material):
return "observed sonar sample material is invalid"
return ""
func _vector_is_finite(value: Vector3) -> bool:
return is_finite(value.x) and is_finite(value.y) and is_finite(value.z)
+115
View File
@@ -0,0 +1,115 @@
class_name SonarSampleBuffer
extends RefCounted
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SonarSample = preload("res://game/domain/sonar/sonar_sample.gd")
var columns: int
var rows: int
var _positions := PackedVector3Array()
var _normals := PackedVector3Array()
var _intensities := PackedFloat32Array()
var _elevations_m := PackedFloat32Array()
var _materials := PackedInt32Array()
var _observed := PackedByteArray()
var _sealed := false
func _init(p_columns: int, p_rows: int) -> void:
columns = p_columns
rows = p_rows
var sample_count := maxi(columns, 0) * maxi(rows, 0)
_positions.resize(sample_count)
_normals.resize(sample_count)
_intensities.resize(sample_count)
_elevations_m.resize(sample_count)
_materials.resize(sample_count)
_observed.resize(sample_count)
for index in sample_count:
_normals[index] = Vector3.UP
_materials[index] = SonarMaterial.Value.UNKNOWN
func set_sample(row: int, column: int, sample: SonarSample) -> String:
if _sealed:
return "sonar sample buffer is sealed"
var index := _index_of(row, column)
if index < 0:
return "sonar sample coordinates are outside the buffer"
if sample == null:
return "sonar sample is missing"
var error := sample.validation_error()
if not error.is_empty():
return error
if not sample.observed:
return "cannot write an unobserved sonar sample"
if _observed[index] == 1:
return "sonar sample coordinates are duplicated"
_positions[index] = sample.position_m
_normals[index] = sample.normal.normalized()
_intensities[index] = sample.intensity
_elevations_m[index] = sample.elevation_m
_materials[index] = sample.material
_observed[index] = 1
return ""
func seal() -> String:
if columns < 2 or rows < 2:
return "sonar sample buffer dimensions must be at least 2 by 2"
_sealed = true
return ""
func is_sealed() -> bool:
return _sealed
func sample_count() -> int:
return columns * rows
func observed_count() -> int:
var count := 0
for value: int in _observed:
count += value
return count
func sample_at(row: int, column: int) -> SonarSample:
var index := _index_of(row, column)
if index < 0 or _observed[index] == 0:
return SonarSample.unknown()
return SonarSample.new(
_positions[index],
_normals[index],
_intensities[index],
_elevations_m[index],
_materials[index],
true
)
func validation_error() -> String:
if columns < 2 or rows < 2:
return "sonar sample buffer dimensions must be at least 2 by 2"
if not _sealed:
return "sonar sample buffer must be sealed before publication"
var expected_size := columns * rows
if (
_positions.size() != expected_size
or _normals.size() != expected_size
or _intensities.size() != expected_size
or _elevations_m.size() != expected_size
or _materials.size() != expected_size
or _observed.size() != expected_size
):
return "sonar sample buffer arrays are not aligned"
return ""
func _index_of(row: int, column: int) -> int:
if row < 0 or row >= rows or column < 0 or column >= columns:
return -1
return row * columns + column
+119
View File
@@ -0,0 +1,119 @@
class_name SonarScan
extends RefCounted
const SonarScanState = preload("res://game/domain/sonar/sonar_scan_state.gd")
const SonarSampleBuffer = preload("res://game/domain/sonar/sonar_sample_buffer.gd")
const SonarContact = preload("res://game/domain/sonar/sonar_contact.gd")
var scan_id: StringName
var session_id: StringName
var sensor_transform: Transform3D
var minimum_range_m: float
var maximum_range_m: float
var horizontal_opening_radians: float
var vertical_opening_radians: float
var reveal_duration_seconds: float
var recovery_duration_seconds: float
var samples: SonarSampleBuffer
var contacts: Array[SonarContact]
var minimum_elevation_m: float
var maximum_elevation_m: float
var state: SonarScanState.Value
var reveal_progress: float
func _init(
p_scan_id: StringName,
p_session_id: StringName,
p_sensor_transform: Transform3D,
p_minimum_range_m: float,
p_maximum_range_m: float,
p_horizontal_opening_radians: float,
p_vertical_opening_radians: float,
p_reveal_duration_seconds: float,
p_recovery_duration_seconds: float,
p_samples: SonarSampleBuffer,
p_contacts: Array[SonarContact],
p_minimum_elevation_m: float,
p_maximum_elevation_m: float,
p_state: SonarScanState.Value,
p_reveal_progress: float
) -> void:
scan_id = p_scan_id
session_id = p_session_id
sensor_transform = p_sensor_transform
minimum_range_m = p_minimum_range_m
maximum_range_m = p_maximum_range_m
horizontal_opening_radians = p_horizontal_opening_radians
vertical_opening_radians = p_vertical_opening_radians
reveal_duration_seconds = p_reveal_duration_seconds
recovery_duration_seconds = p_recovery_duration_seconds
samples = p_samples
contacts = p_contacts.duplicate()
minimum_elevation_m = p_minimum_elevation_m
maximum_elevation_m = p_maximum_elevation_m
state = p_state
reveal_progress = p_reveal_progress
func with_phase(next_state: SonarScanState.Value, next_progress: float) -> SonarScan:
return new(
scan_id,
session_id,
sensor_transform,
minimum_range_m,
maximum_range_m,
horizontal_opening_radians,
vertical_opening_radians,
reveal_duration_seconds,
recovery_duration_seconds,
samples,
contacts,
minimum_elevation_m,
maximum_elevation_m,
next_state,
next_progress
)
func validation_error() -> String:
if scan_id.is_empty() or session_id.is_empty():
return "sonar scan identifiers must not be empty"
if not is_finite(minimum_range_m) or minimum_range_m < 0.0:
return "sonar minimum range is invalid"
if not is_finite(maximum_range_m) or maximum_range_m <= minimum_range_m:
return "sonar maximum range must exceed minimum range"
if (
not is_finite(horizontal_opening_radians)
or horizontal_opening_radians <= 0.0
or horizontal_opening_radians >= PI
):
return "sonar horizontal opening is invalid"
if (
not is_finite(vertical_opening_radians)
or vertical_opening_radians <= 0.0
or vertical_opening_radians >= PI
):
return "sonar vertical opening is invalid"
if reveal_duration_seconds <= 0.0 or recovery_duration_seconds < 0.0:
return "sonar phase durations are invalid"
if samples == null:
return "sonar sample buffer is missing"
var buffer_error := samples.validation_error()
if not buffer_error.is_empty():
return buffer_error
for contact: SonarContact in contacts:
if contact == null:
return "sonar contact is missing"
var contact_error := contact.validation_error()
if not contact_error.is_empty():
return contact_error
if not is_finite(minimum_elevation_m) or not is_finite(maximum_elevation_m):
return "sonar elevation scale is not finite"
if maximum_elevation_m <= minimum_elevation_m:
return "sonar elevation scale is empty"
if not SonarScanState.is_valid(state):
return "sonar scan state is invalid"
if not is_finite(reveal_progress) or reveal_progress < 0.0 or reveal_progress > 1.0:
return "sonar reveal progress must be between 0 and 1"
return ""
+13
View File
@@ -0,0 +1,13 @@
class_name SonarScanState
extends RefCounted
enum Value {
READY,
CAPTURE,
REVEALING,
RECOVERY,
}
static func is_valid(value: int) -> bool:
return value >= Value.READY and value <= Value.RECOVERY