Project

General

Profile

User Story #40 » 0006-aiming-MeshAimTracker-pan-around-validation-Gap-3.patch

knight8241, 08/07/2026 18:32

View differences:

common/src/commonMain/kotlin/com/aether/mofe/engine/netcode/MeshAimTracker.kt
package com.aether.mofe.engine.netcode
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.Timestamp
import com.aether.mofe.model.Vector3D
/**
* Client-side, semi-real-time aim tracker over a shared mesh reality — the *initial*
* goal of the aim work, and the foundation the aim *event* handler builds on.
*
* For an individual client it places every mesh object on the observer's reticle from
* **entity-interpolated** positions ([NetcodeSession.renderPose]): aiming at an object's
* solved location shows it dead-center (reticle offset (0,0)) **by construction**, from
* any range or angle, and the object's ~10 Hz position jitter is dissolved by
* interpolation so the track feels near-real-time and does not stutter. Absolute
* self-heading accuracy is a separate axis — this guarantees the viewer is
* self-consistent: aim at the solved location, see the target there.
*
* This is the *tracker* — continuous, per-client rendering. Detecting that a device is
* aiming at another object and arbitrating that as a discrete claim/verdict is a distinct
* concern that *follows* from this; it does not belong here.
*
* Pure: it reads the interpolation buffers plus the observer's supplied aim basis — no IO,
* no clock. [facing] is the observer's aim ray (body +Y, the engine's forward convention);
* [up] is the viewer's up vector.
*/
class MeshAimTracker(private val session: NetcodeSession) {
data class Track(
val deviceId: DeviceId,
/** Angle between the aim ray and the direction to the object; 0 = dead-center. */
val aimErrorRadians: Double,
/** (right, up) reticle offset normalized to the forward component — (0,0) when
* aimed at the object's solved location; null when it is at/behind the horizon. */
val reticleOffset: Pair<Double, Double>?,
/** Whether the object falls within the reticle cone of [track]/[tracks]. */
val onReticle: Boolean,
)
/** Track a single [target] at [nowMesh]; null until it has interpolable history. */
fun track(
target: DeviceId,
observerPos: Vector3D,
facing: Vector3D,
up: Vector3D,
nowMesh: Timestamp,
coneRadians: Double,
): Track? {
val pose = session.renderPose(target, nowMesh) ?: return null
return placement(target, observerPos, facing, up, pose.position, coneRadians)
}
/** Track every mesh object the session has history for, at [nowMesh]. */
fun tracks(
observerPos: Vector3D,
facing: Vector3D,
up: Vector3D,
nowMesh: Timestamp,
coneRadians: Double,
): List<Track> = session.trackedDevices().mapNotNull { id ->
track(id, observerPos, facing, up, nowMesh, coneRadians)
}
private fun placement(
id: DeviceId,
observerPos: Vector3D,
facing: Vector3D,
up: Vector3D,
targetPos: Vector3D,
coneRadians: Double,
): Track {
val err = AimEvaluator.aimErrorRadians(observerPos, facing, targetPos)
return Track(
deviceId = id,
aimErrorRadians = err,
reticleOffset = AimEvaluator.facingFrameOffset(observerPos, facing, up, targetPos),
onReticle = AimEvaluator.withinCone(err, coneRadians),
)
}
}
common/src/commonTest/kotlin/com/aether/mofe/engine/netcode/MeshAimTrackerTest.kt
package com.aether.mofe.engine.netcode
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.Quaternion
import com.aether.mofe.model.Timestamp
import com.aether.mofe.model.Vector3D
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.tan
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* Pan-around validation for [MeshAimTracker] — the roadmap's Phase-0 exit criterion for the
* view. [AimEvaluatorTest] covers the pure aim geometry; this exercises the *tracker's*
* integration of entity-interpolated [NetcodeSession.renderPose] with that geometry:
* - the by-construction dead-center guarantee holds on an *interpolated* pose,
* - a pan sweep crosses the reticle smoothly and monotonically,
* - ~10 Hz position jitter is dissolved into a continuous (non-stutter) track,
* - the tracked pose lags "now" by exactly the quality-derived interpolation delay.
*
* The buffer retains [TemporalStateBuffer.DEFAULT_RETENTION_MICROS] = 1 s, so every scenario
* is kept inside a [0, 0.9 s] window and queried at `now ≤ 0.85 s`; `now − interpDelay` then
* lands strictly between samples (interpolating), never clamped to the retained edge.
*/
class MeshAimTrackerTest {
private val up = Vector3D(0.0, 0.0, 1.0) // world up; facing is kept in XY so the basis is non-degenerate
private val deg = PI / 180.0
private val base = 100_000L // interp-delay base; an unsynced session sits at POOR ⇒ 2.5× this
/** Record device [id] moving linearly (p0 + vel·t) over [t0,t1] at [stepMicros] cadence. */
private fun NetcodeSession.recordLinear(
id: String, p0: Vector3D, vel: Vector3D, t0: Long, t1: Long, stepMicros: Long,
) {
var t = t0
while (t <= t1) {
val p = p0 + vel * ((t - t0) / 1_000_000.0)
recordSample(DeviceId(id), Timestamp(t), TemporalPose(p, vel, Quaternion.IDENTITY))
t += stepMicros
}
}
@Test
fun trackIsNullForADeviceWithNoHistory() {
val tracker = MeshAimTracker(NetcodeSession())
assertNull(
tracker.track(DeviceId("ghost"), Vector3D.ZERO, Vector3D(1.0, 0.0, 0.0), up, Timestamp(1_000), 0.1),
"a device the session has never seen has no interpolable pose",
)
}
@Test
fun deadCenterWhenAimedAtTheInterpolatedPosition() {
val s = NetcodeSession()
// A target gliding along +Y at 2 m/s, ~10 m out along +X; `now − delay` lands off a sample
// boundary so renderPose must interpolate, not echo a raw sample.
s.recordLinear("t", Vector3D(10.0, -1.0, 0.0), Vector3D(0.0, 2.0, 0.0), 0, 900_000, 100_000)
val tracker = MeshAimTracker(s)
val now = Timestamp(630_000) // now − 250 ms delay = 380 ms, strictly between 100 ms samples
val observer = Vector3D.ZERO
val interp = s.renderPose(DeviceId("t"), now)!!.position // the entity-interpolated location
val track = tracker.track(DeviceId("t"), observer, interp - observer, up, now, coneRadians = 0.01)!!
assertTrue(track.aimErrorRadians < 1e-6, "aim error ~0 when aimed at the interpolated location")
val (right, upOff) = track.reticleOffset!!
assertEquals(0.0, right, 1e-6, "dead-center (right) by construction")
assertEquals(0.0, upOff, 1e-6, "dead-center (up) by construction")
assertTrue(track.onReticle)
}
@Test
fun panSweepCrossesTheReticleSmoothlyAndMonotonically() {
val s = NetcodeSession()
s.recordLinear("t", Vector3D(10.0, 0.0, 0.0), Vector3D.ZERO, 0, 900_000, 100_000)
val tracker = MeshAimTracker(s)
val now = Timestamp(650_000)
val observer = Vector3D.ZERO
val cone = 5.0 * deg
var prevRight = Double.NEGATIVE_INFINITY
for (i in -20..20) {
val theta = i * 2.0 * deg // pan −40°..+40° about world up
val facing = Vector3D(cos(theta), sin(theta), 0.0)
val tr = tracker.track(DeviceId("t"), observer, facing, up, now, cone)!!
assertEquals(abs(theta), tr.aimErrorRadians, 1e-6, "aim error equals the pan magnitude")
val (right, upOff) = tr.reticleOffset!!
assertEquals(tan(theta), right, 1e-6, "reticle offset (right) = tan(pan angle)")
assertEquals(0.0, upOff, 1e-6, "pan about up leaves the up-offset at 0")
assertTrue(right > prevRight, "reticle offset sweeps monotonically through 0")
prevRight = right
assertEquals(abs(theta) <= cone, tr.onReticle, "onReticle gates on the 5° cone")
}
}
@Test
fun tenHzJitterIsDissolvedIntoAContinuousTrack() {
val s = NetcodeSession()
// A target ~10 m out drifting +Y at 1 m/s, sampled at 10 Hz with ±5 cm deterministic jitter.
var t = 0L
while (t <= 900_000) {
val settled = Vector3D(10.0, 1.0 * (t / 1_000_000.0), 0.0)
val jitter = Vector3D(0.0, 0.05 * sin(t / 60_000.0), 0.03 * cos(t / 71_000.0))
s.recordSample(DeviceId("t"), Timestamp(t), TemporalPose(settled + jitter, Vector3D(0.0, 1.0, 0.0), Quaternion.IDENTITY))
t += 100_000 // 10 Hz
}
val tracker = MeshAimTracker(s)
val observer = Vector3D.ZERO
val facing = Vector3D(1.0, 0.0, 0.0)
// Advance "now" in fine 2 ms steps (interp delay is constant, so `now − delay` sweeps
// between the same 10 Hz samples). A tracker that snapped to the nearest raw sample would
// jump ~atan(0.1/10) ≈ 1e-2 rad at each 100 ms boundary; linear interpolation stays far below.
var prev: Double? = null
var maxStep = 0.0
var now = 600_000L
while (now <= 750_000) {
val err = tracker.track(DeviceId("t"), observer, facing, up, Timestamp(now), 0.1)!!.aimErrorRadians
prev?.let { maxStep = maxOf(maxStep, abs(err - it)) }
prev = err
now += 2_000
}
assertTrue(maxStep < 2e-3, "tracked bearing is continuous — largest 2 ms step was $maxStep rad (no stutter)")
}
@Test
fun tracksEveryDeviceAndGatesEachOnItsCone() {
val s = NetcodeSession()
s.recordLinear("ahead", Vector3D(10.0, 0.0, 0.0), Vector3D.ZERO, 0, 900_000, 100_000) // on-axis
s.recordLinear("beside", Vector3D(0.0, 10.0, 0.0), Vector3D.ZERO, 0, 900_000, 100_000) // 90° off
val tracker = MeshAimTracker(s)
val tracks = tracker.tracks(Vector3D.ZERO, Vector3D(1.0, 0.0, 0.0), up, Timestamp(650_000), coneRadians = 10.0 * deg)
assertEquals(setOf(DeviceId("ahead"), DeviceId("beside")), tracks.map { it.deviceId }.toSet())
assertTrue(tracks.first { it.deviceId == DeviceId("ahead") }.onReticle, "on-axis target is on the reticle")
assertFalse(tracks.first { it.deviceId == DeviceId("beside") }.onReticle, "90°-off target is not")
}
@Test
fun trackedPoseLagsNowByExactlyTheInterpDelay() {
val s = NetcodeSession(baseInterpDelayMicros = base)
val vel = Vector3D(0.0, 4.0, 0.0) // 4 m/s so the lag shows up as a boundable position offset
s.recordLinear("t", Vector3D(10.0, 0.0, 0.0), vel, 0, 900_000, 50_000)
val tracker = MeshAimTracker(s)
val now = Timestamp(650_000)
// Derive the delay from the session's OWN quality tier + base — no hardcoded multiplier.
val delayMicros = MeshQuality.interpDelayMicros(s.quality(now), base)
assertTrue(delayMicros > 0, "an unsynced session renders in the past (POOR tier widens the delay)")
val tracked = s.renderPose(DeviceId("t"), now)!!.position
val truthNow = Vector3D(10.0, 4.0 * (now.microseconds / 1_000_000.0), 0.0) // where it really is at `now`
val lagMeters = (truthNow - tracked).magnitude
val expectedLag = vel.magnitude * (delayMicros / 1_000_000.0)
assertEquals(expectedLag, lagMeters, 1e-6, "tracked pose lags now by exactly the interp delay")
}
}
    (1-1/1)