mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
fix(cockpit): an application subject's eye eases off the floor instead of jumping: its anchor is no longer clamped to the floor, and the eye's clearance above it changes at most 1.5 m/s (easeCockpitLift)
This commit is contained in:
1 parent
f75179eea0
commit
01a74fceec
4 files changed
+225
-47
No files matched your search
@@ -68,6 +68,42 @@ export function cockpitSurfaceWaitExpired(nowMs, startedMs, timeoutMs = 5000) {
|
|||||||
return nowMs - startedMs >= timeoutMs;
|
return nowMs - startedMs >= timeoutMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How fast the clearance a ground clamp adds under an application subject's
|
||||||
|
* eye comes and goes (m/s).
|
||||||
|
*/
|
||||||
|
export const COCKPIT_SUBJECT_LIFT_MPS = 1.5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lift the ground clamp gives an application subject's eye this update.
|
||||||
|
* The floor is read from coarse cells that refine as tiles load, so the lift
|
||||||
|
* it asks for jumps; a ridden eye takes the clearance part of it at
|
||||||
|
* `rateMps`, while what it needs to stay out of the floor itself (`hardM`)
|
||||||
|
* applies at once.
|
||||||
|
* @param {number|null} previousM The lift given last update; null on entry.
|
||||||
|
* @param {number} targetM The lift the floor and clearance ask for (m).
|
||||||
|
* @param {number} hardM The lift that keeps the eye out of the floor (m).
|
||||||
|
* @param {number} dtSec Seconds since the last update.
|
||||||
|
* @param {number} [rateMps]
|
||||||
|
* @returns {number} At least 0, at least `hardM`.
|
||||||
|
*/
|
||||||
|
export function easeCockpitLift(
|
||||||
|
previousM,
|
||||||
|
targetM,
|
||||||
|
hardM,
|
||||||
|
dtSec,
|
||||||
|
rateMps = COCKPIT_SUBJECT_LIFT_MPS,
|
||||||
|
) {
|
||||||
|
const target = Number.isFinite(targetM) ? Math.max(0, targetM) : 0;
|
||||||
|
const hard = Number.isFinite(hardM) ? Math.max(0, hardM) : 0;
|
||||||
|
if (!Number.isFinite(previousM)) return Math.max(target, hard);
|
||||||
|
const step =
|
||||||
|
Math.max(0, Number.isFinite(rateMps) ? rateMps : 0) *
|
||||||
|
Math.max(0, Number.isFinite(dtSec) ? dtSec : 0);
|
||||||
|
const eased = previousM + Math.max(-step, Math.min(step, target - previousM));
|
||||||
|
return Math.max(0, hard, eased);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep the cockpit camera above the shared rendered-surface floor.
|
* Keep the cockpit camera above the shared rendered-surface floor.
|
||||||
* Unknown floors preserve the proposed camera height.
|
* Unknown floors preserve the proposed camera height.
|
||||||
|
|||||||
+151
-44
@@ -10,6 +10,8 @@ import {
|
|||||||
cockpitGroundSafeHeight,
|
cockpitGroundSafeHeight,
|
||||||
cockpitSurfaceWaitExpired,
|
cockpitSurfaceWaitExpired,
|
||||||
cockpitUiUpdateDue,
|
cockpitUiUpdateDue,
|
||||||
|
COCKPIT_SUBJECT_LIFT_MPS,
|
||||||
|
easeCockpitLift,
|
||||||
compassDivisions,
|
compassDivisions,
|
||||||
compassMarkerSlot,
|
compassMarkerSlot,
|
||||||
scrollingCompassDivisions,
|
scrollingCompassDivisions,
|
||||||
@@ -45,7 +47,11 @@ test('cockpit anchor correction cannot turn a forward step into a reversal', ()
|
|||||||
const speedMps = 200;
|
const speedMps = 200;
|
||||||
const dtSec = 0.1;
|
const dtSec = 0.1;
|
||||||
const forwardStepM = speedMps * dtSec;
|
const forwardStepM = speedMps * dtSec;
|
||||||
const backwardCorrectionM = cockpitAnchorCorrectionStep(1000, speedMps, dtSec);
|
const backwardCorrectionM = cockpitAnchorCorrectionStep(
|
||||||
|
1000,
|
||||||
|
speedMps,
|
||||||
|
dtSec,
|
||||||
|
);
|
||||||
assert.ok(backwardCorrectionM > 0);
|
assert.ok(backwardCorrectionM > 0);
|
||||||
assert.ok(backwardCorrectionM < forwardStepM);
|
assert.ok(backwardCorrectionM < forwardStepM);
|
||||||
});
|
});
|
||||||
@@ -81,10 +87,7 @@ test('cockpit Context scope distinguishes radius-complete feeds from viewport in
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('cockpit Context scope preserves zero and replaces missing values intentionally', () => {
|
test('cockpit Context scope preserves zero and replaces missing values intentionally', () => {
|
||||||
assert.equal(
|
assert.equal(formatCockpitContextScope('', 0), '— · 0 KM AIR/SEA WINDOW');
|
||||||
formatCockpitContextScope('', 0),
|
|
||||||
'— · 0 KM AIR/SEA WINDOW',
|
|
||||||
);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatCockpitContextScope(undefined, Number.NaN, {}),
|
formatCockpitContextScope(undefined, Number.NaN, {}),
|
||||||
'— · — KM AIR/SEA WINDOW',
|
'— · — KM AIR/SEA WINDOW',
|
||||||
@@ -221,26 +224,29 @@ test('relative bearing stays on the shortest signed arc', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('HUD rail layout centers between intersecting upper and lower obstacles', () => {
|
test('HUD rail layout centers between intersecting upper and lower obstacles', () => {
|
||||||
assert.deepEqual(resolveHudRailLayout({
|
assert.deepEqual(
|
||||||
viewportHeight: 1000,
|
resolveHudRailLayout({
|
||||||
panelHeight: 200,
|
viewportHeight: 1000,
|
||||||
laneLeft: 50,
|
panelHeight: 200,
|
||||||
laneRight: 410,
|
laneLeft: 50,
|
||||||
baseTop: 280,
|
laneRight: 410,
|
||||||
baseBottom: 940,
|
baseTop: 280,
|
||||||
gap: 10,
|
baseBottom: 940,
|
||||||
obstacles: [
|
gap: 10,
|
||||||
{ left: 40, right: 240, top: 320, bottom: 460 },
|
obstacles: [
|
||||||
{ left: 300, right: 1000, top: 780, bottom: 930 },
|
{ left: 40, right: 240, top: 320, bottom: 460 },
|
||||||
{ left: 900, right: 1100, top: 300, bottom: 700 },
|
{ left: 300, right: 1000, top: 780, bottom: 930 },
|
||||||
],
|
{ left: 900, right: 1100, top: 300, bottom: 700 },
|
||||||
}), {
|
],
|
||||||
top: 520,
|
}),
|
||||||
maxHeight: 300,
|
{
|
||||||
safeTop: 470,
|
top: 520,
|
||||||
safeBottom: 770,
|
maxHeight: 300,
|
||||||
constrained: false,
|
safeTop: 470,
|
||||||
});
|
safeBottom: 770,
|
||||||
|
constrained: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('HUD rail layout constrains an oversized panel to the safe corridor', () => {
|
test('HUD rail layout constrains an oversized panel to the safe corridor', () => {
|
||||||
@@ -294,7 +300,10 @@ function contextSnapshot(subject, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('Contact panel hides only when there is no snapshot at all', () => {
|
test('Contact panel hides only when there is no snapshot at all', () => {
|
||||||
const readout = resolveCockpitContextReadout({ snapshot: null, info: TRACKED_INFO });
|
const readout = resolveCockpitContextReadout({
|
||||||
|
snapshot: null,
|
||||||
|
info: TRACKED_INFO,
|
||||||
|
});
|
||||||
assert.equal(readout.visible, false);
|
assert.equal(readout.visible, false);
|
||||||
assert.equal(readout.mode, 'standby');
|
assert.equal(readout.mode, 'standby');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -310,14 +319,22 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () =>
|
|||||||
// camera: "click next... whole left panel disappears").
|
// camera: "click next... whole left panel disappears").
|
||||||
for (const subject of [
|
for (const subject of [
|
||||||
{ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' },
|
{ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' },
|
||||||
{ layerId: 'military-installations', id: 'fort-hood', label: 'FORT CAVAZOS' },
|
{
|
||||||
|
layerId: 'military-installations',
|
||||||
|
id: 'fort-hood',
|
||||||
|
label: 'FORT CAVAZOS',
|
||||||
|
},
|
||||||
{ layerId: 'military', id: 'ae01ce', label: 'RCH451' },
|
{ layerId: 'military', id: 'ae01ce', label: 'RCH451' },
|
||||||
]) {
|
]) {
|
||||||
const readout = resolveCockpitContextReadout({
|
const readout = resolveCockpitContextReadout({
|
||||||
snapshot: contextSnapshot(subject),
|
snapshot: contextSnapshot(subject),
|
||||||
info: TRACKED_INFO,
|
info: TRACKED_INFO,
|
||||||
});
|
});
|
||||||
assert.equal(readout.visible, true, `${subject.layerId} subject must keep the panel up`);
|
assert.equal(
|
||||||
|
readout.visible,
|
||||||
|
true,
|
||||||
|
`${subject.layerId} subject must keep the panel up`,
|
||||||
|
);
|
||||||
assert.equal(readout.mode, 'foreign');
|
assert.equal(readout.mode, 'foreign');
|
||||||
assert.equal(readout.subjectMatchesTracked, false);
|
assert.equal(readout.subjectMatchesTracked, false);
|
||||||
}
|
}
|
||||||
@@ -325,7 +342,11 @@ test('Contact panel survives NEXT onto a vessel or installation subject', () =>
|
|||||||
|
|
||||||
test('a foreign subject dashes the aircraft-relative fields and keeps the rest live', () => {
|
test('a foreign subject dashes the aircraft-relative fields and keeps the rest live', () => {
|
||||||
const readout = resolveCockpitContextReadout({
|
const readout = resolveCockpitContextReadout({
|
||||||
snapshot: contextSnapshot({ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' }),
|
snapshot: contextSnapshot({
|
||||||
|
layerId: 'ais-live-vessels',
|
||||||
|
id: '353136000',
|
||||||
|
label: 'MAERSK DETROIT',
|
||||||
|
}),
|
||||||
info: TRACKED_INFO,
|
info: TRACKED_INFO,
|
||||||
});
|
});
|
||||||
assert.equal(readout.visible, true);
|
assert.equal(readout.visible, true);
|
||||||
@@ -334,12 +355,20 @@ test('a foreign subject dashes the aircraft-relative fields and keeps the rest l
|
|||||||
// frame while every other row value is measured from the subject; rendering
|
// frame while every other row value is measured from the subject; rendering
|
||||||
// both live would present one mixed-frame reading as a single measurement.
|
// both live would present one mixed-frame reading as a single measurement.
|
||||||
assert.equal(readout.aircraftRelative, false);
|
assert.equal(readout.aircraftRelative, false);
|
||||||
assert.equal(readout.contactLost, false, 'subject-frame values still refresh');
|
assert.equal(
|
||||||
|
readout.contactLost,
|
||||||
|
false,
|
||||||
|
'subject-frame values still refresh',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the tracked aircraft as subject keeps every field in its own frame', () => {
|
test('the tracked aircraft as subject keeps every field in its own frame', () => {
|
||||||
const readout = resolveCockpitContextReadout({
|
const readout = resolveCockpitContextReadout({
|
||||||
snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077', label: 'SWA1234' }),
|
snapshot: contextSnapshot({
|
||||||
|
layerId: 'flights',
|
||||||
|
id: 'aaa077',
|
||||||
|
label: 'SWA1234',
|
||||||
|
}),
|
||||||
info: TRACKED_INFO,
|
info: TRACKED_INFO,
|
||||||
});
|
});
|
||||||
assert.equal(readout.visible, true);
|
assert.equal(readout.visible, true);
|
||||||
@@ -358,7 +387,11 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap
|
|||||||
snapshot: contextSnapshot(subject, { subjectPresent: false }),
|
snapshot: contextSnapshot(subject, { subjectPresent: false }),
|
||||||
info: TRACKED_INFO,
|
info: TRACKED_INFO,
|
||||||
});
|
});
|
||||||
assert.equal(readout.visible, true, 'a lost contact must not take the panel down');
|
assert.equal(
|
||||||
|
readout.visible,
|
||||||
|
true,
|
||||||
|
'a lost contact must not take the panel down',
|
||||||
|
);
|
||||||
assert.equal(readout.mode, 'lost');
|
assert.equal(readout.mode, 'lost');
|
||||||
assert.equal(readout.contactLost, true, 'last-known values stay on screen');
|
assert.equal(readout.contactLost, true, 'last-known values stay on screen');
|
||||||
}
|
}
|
||||||
@@ -366,7 +399,10 @@ test('a fast-culled subject keeps the panel up as CONTACT LOST instead of collap
|
|||||||
|
|
||||||
test('an explicitly present subject is never reported lost', () => {
|
test('an explicitly present subject is never reported lost', () => {
|
||||||
const present = resolveCockpitContextReadout({
|
const present = resolveCockpitContextReadout({
|
||||||
snapshot: contextSnapshot({ layerId: 'flights', id: 'aaa077' }, { subjectPresent: true }),
|
snapshot: contextSnapshot(
|
||||||
|
{ layerId: 'flights', id: 'aaa077' },
|
||||||
|
{ subjectPresent: true },
|
||||||
|
),
|
||||||
info: TRACKED_INFO,
|
info: TRACKED_INFO,
|
||||||
});
|
});
|
||||||
assert.equal(present.contactLost, false);
|
assert.equal(present.contactLost, false);
|
||||||
@@ -385,20 +421,36 @@ test('the tracked flight layer is resolved by normalized tracked identity', () =
|
|||||||
// Both layers describe a tracked aircraft during a cross-layer handoff;
|
// Both layers describe a tracked aircraft during a cross-layer handoff;
|
||||||
// civilian-first precedence would hand the cockpit the wrong aircraft.
|
// civilian-first precedence would hand the cockpit the wrong aircraft.
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).layerId,
|
resolveTrackedAircraftInfo({
|
||||||
|
civilian,
|
||||||
|
military,
|
||||||
|
trackedId: 'military:ae01ce',
|
||||||
|
}).layerId,
|
||||||
'military',
|
'military',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'military:ae01ce' }).icao24,
|
resolveTrackedAircraftInfo({
|
||||||
|
civilian,
|
||||||
|
military,
|
||||||
|
trackedId: 'military:ae01ce',
|
||||||
|
}).icao24,
|
||||||
'ae01ce',
|
'ae01ce',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'flights:aaa077' }).layerId,
|
resolveTrackedAircraftInfo({
|
||||||
|
civilian,
|
||||||
|
military,
|
||||||
|
trackedId: 'flights:aaa077',
|
||||||
|
}).layerId,
|
||||||
'flights',
|
'flights',
|
||||||
);
|
);
|
||||||
// Case-insensitive: layers stamp lowercase hex, callers may not.
|
// Case-insensitive: layers stamp lowercase hex, callers may not.
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'MILITARY:AE01CE' }).layerId,
|
resolveTrackedAircraftInfo({
|
||||||
|
civilian,
|
||||||
|
military,
|
||||||
|
trackedId: 'MILITARY:AE01CE',
|
||||||
|
}).layerId,
|
||||||
'military',
|
'military',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -408,30 +460,85 @@ test('tracked-identity resolution falls back to layer precedence', () => {
|
|||||||
const military = { icao24: 'ae01ce' };
|
const military = { icao24: 'ae01ce' };
|
||||||
// No stamped identity (or one that matches neither layer) keeps the historic
|
// No stamped identity (or one that matches neither layer) keeps the historic
|
||||||
// civilian-first order so tracking paths without gevTrackedId still work.
|
// civilian-first order so tracking paths without gevTrackedId still work.
|
||||||
assert.equal(resolveTrackedAircraftInfo({ civilian, military }).layerId, 'flights');
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
resolveTrackedAircraftInfo({ civilian, military, trackedId: 'satellites:25544' }).layerId,
|
resolveTrackedAircraftInfo({ civilian, military }).layerId,
|
||||||
'flights',
|
'flights',
|
||||||
);
|
);
|
||||||
assert.equal(resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId, 'military');
|
assert.equal(
|
||||||
assert.equal(resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }), null);
|
resolveTrackedAircraftInfo({
|
||||||
|
civilian,
|
||||||
|
military,
|
||||||
|
trackedId: 'satellites:25544',
|
||||||
|
}).layerId,
|
||||||
|
'flights',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveTrackedAircraftInfo({ military, trackedId: '' }).layerId,
|
||||||
|
'military',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveTrackedAircraftInfo({ trackedId: 'flights:aaa077' }),
|
||||||
|
null,
|
||||||
|
);
|
||||||
assert.equal(resolveTrackedAircraftInfo(), null);
|
assert.equal(resolveTrackedAircraftInfo(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cockpit emergency wording states the broadcast code, not a conclusion', () => {
|
test('cockpit emergency wording states the broadcast code, not a conclusion', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatCockpitEmergency({ label: 'General emergency', squawk: '7700', source: 'squawk', severity: 'emergency' }),
|
formatCockpitEmergency({
|
||||||
|
label: 'General emergency',
|
||||||
|
squawk: '7700',
|
||||||
|
source: 'squawk',
|
||||||
|
severity: 'emergency',
|
||||||
|
}),
|
||||||
'SQUAWK 7700 · GENERAL EMERGENCY',
|
'SQUAWK 7700 · GENERAL EMERGENCY',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatCockpitEmergency({ label: 'Downed aircraft', squawk: '1200', source: 'ads-b', severity: 'emergency' }),
|
formatCockpitEmergency({
|
||||||
|
label: 'Downed aircraft',
|
||||||
|
squawk: '1200',
|
||||||
|
source: 'ads-b',
|
||||||
|
severity: 'emergency',
|
||||||
|
}),
|
||||||
'ADS-B EMERGENCY · DOWNED AIRCRAFT',
|
'ADS-B EMERGENCY · DOWNED AIRCRAFT',
|
||||||
'an unrelated squawk is not quoted beside an ADS-B status',
|
'an unrelated squawk is not quoted beside an ADS-B status',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatCockpitEmergency({ label: 'Minimum fuel', squawk: null, source: 'ads-b', severity: 'priority' }),
|
formatCockpitEmergency({
|
||||||
|
label: 'Minimum fuel',
|
||||||
|
squawk: null,
|
||||||
|
source: 'ads-b',
|
||||||
|
severity: 'priority',
|
||||||
|
}),
|
||||||
'ADS-B PRIORITY · MINIMUM FUEL',
|
'ADS-B PRIORITY · MINIMUM FUEL',
|
||||||
);
|
);
|
||||||
assert.equal(formatCockpitEmergency(null), null);
|
assert.equal(formatCockpitEmergency(null), null);
|
||||||
assert.equal(formatCockpitEmergency({ label: '' }), null);
|
assert.equal(formatCockpitEmergency({ label: '' }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an application subject’s ground lift eases its clearance, never the floor', () => {
|
||||||
|
// On entry the lift asked for applies at once.
|
||||||
|
assert.equal(easeCockpitLift(null, 1.5, 0, 0.016), 1.5);
|
||||||
|
// A floor cell that rises by a metre is met at the lift rate...
|
||||||
|
let lift = 0;
|
||||||
|
const frames = [];
|
||||||
|
for (let i = 0; i < 60; i += 1) {
|
||||||
|
lift = easeCockpitLift(lift, 1, 0, 0.016);
|
||||||
|
frames.push(lift);
|
||||||
|
}
|
||||||
|
assert.ok(frames[0] <= COCKPIT_SUBJECT_LIFT_MPS * 0.016 + 1e-9);
|
||||||
|
for (let i = 1; i < frames.length; i += 1)
|
||||||
|
assert.ok(
|
||||||
|
frames[i] - frames[i - 1] <= COCKPIT_SUBJECT_LIFT_MPS * 0.016 + 1e-9,
|
||||||
|
);
|
||||||
|
assert.ok(Math.abs(frames.at(-1) - 1) < 1e-9, 'reached in under a second');
|
||||||
|
// ...and given back the same way.
|
||||||
|
assert.ok(Math.abs(easeCockpitLift(1, 0, 0, 0.1) - 0.85) < 1e-9);
|
||||||
|
// What keeps the eye out of the floor itself is never eased.
|
||||||
|
assert.equal(easeCockpitLift(0, 201.5, 200, 0.016), 200);
|
||||||
|
assert.equal(
|
||||||
|
easeCockpitLift(0.2, -3, -3, 0.016),
|
||||||
|
0.2 - COCKPIT_SUBJECT_LIFT_MPS * 0.016,
|
||||||
|
);
|
||||||
|
assert.equal(easeCockpitLift(0, Number.NaN, Number.NaN, 0.016), 0);
|
||||||
|
});
|
||||||
+29
-2
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
|
|||||||
import {
|
import {
|
||||||
cockpitAnchorCorrectionStep,
|
cockpitAnchorCorrectionStep,
|
||||||
cockpitGroundSafeHeight,
|
cockpitGroundSafeHeight,
|
||||||
|
easeCockpitLift,
|
||||||
cockpitSurfaceWaitExpired,
|
cockpitSurfaceWaitExpired,
|
||||||
cockpitUiUpdateDue,
|
cockpitUiUpdateDue,
|
||||||
slewHeading,
|
slewHeading,
|
||||||
@@ -412,6 +413,7 @@ function placeCamera(target, info, nowMs, dtSec, profile, look = null) {
|
|||||||
if (!this.cockpitAnchorValid) {
|
if (!this.cockpitAnchorValid) {
|
||||||
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
|
Cesium.Cartesian3.clone(target, this.cockpitAnchor);
|
||||||
this.cockpitAnchorValid = true;
|
this.cockpitAnchorValid = true;
|
||||||
|
this.subjectLiftM = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First-person motion cannot use the delayed feed correction as a raw
|
// First-person motion cannot use the delayed feed correction as a raw
|
||||||
@@ -595,7 +597,14 @@ function placeCamera(target, info, nowMs, dtSec, profile, look = null) {
|
|||||||
cockpitFloorM =
|
cockpitFloorM =
|
||||||
targetCartographic.height - this.services.GROUND_FLOOR_LIFT_M;
|
targetCartographic.height - this.services.GROUND_FLOOR_LIFT_M;
|
||||||
}
|
}
|
||||||
if (groundClamp && anchorCartographic && Number.isFinite(cockpitFloorM)) {
|
// An application subject's anchor is where its provider puts it; only its
|
||||||
|
// eye is kept out of the floor, below.
|
||||||
|
if (
|
||||||
|
groundClamp &&
|
||||||
|
info.generic !== true &&
|
||||||
|
anchorCartographic &&
|
||||||
|
Number.isFinite(cockpitFloorM)
|
||||||
|
) {
|
||||||
const minimumAnchorHeightM = cockpitGroundSafeHeight(
|
const minimumAnchorHeightM = cockpitGroundSafeHeight(
|
||||||
anchorCartographic.height,
|
anchorCartographic.height,
|
||||||
cockpitFloorM,
|
cockpitFloorM,
|
||||||
@@ -686,11 +695,26 @@ function placeCamera(target, info, nowMs, dtSec, profile, look = null) {
|
|||||||
cameraFloorM,
|
cameraFloorM,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const safeHeightM = cockpitGroundSafeHeight(
|
let safeHeightM = cockpitGroundSafeHeight(
|
||||||
cameraCartographic.height,
|
cameraCartographic.height,
|
||||||
cockpitFloorM,
|
cockpitFloorM,
|
||||||
profile.minClearanceM,
|
profile.minClearanceM,
|
||||||
);
|
);
|
||||||
|
// An application subject's eye rides its provider's smoothed height:
|
||||||
|
// the clearance over the coarse, refining floor eases in and out
|
||||||
|
// rather than jumping at each cell (easeCockpitLift).
|
||||||
|
if (info.generic === true) {
|
||||||
|
const hardM =
|
||||||
|
cockpitGroundSafeHeight(cameraCartographic.height, cockpitFloorM, 0) -
|
||||||
|
cameraCartographic.height;
|
||||||
|
this.subjectLiftM = easeCockpitLift(
|
||||||
|
this.subjectLiftM,
|
||||||
|
safeHeightM - cameraCartographic.height,
|
||||||
|
hardM,
|
||||||
|
dtSec,
|
||||||
|
);
|
||||||
|
safeHeightM = cameraCartographic.height + this.subjectLiftM;
|
||||||
|
}
|
||||||
if (safeHeightM !== cameraCartographic.height) {
|
if (safeHeightM !== cameraCartographic.height) {
|
||||||
cameraCartographic.height = safeHeightM;
|
cameraCartographic.height = safeHeightM;
|
||||||
Cesium.Ellipsoid.WGS84.cartographicToCartesian(
|
Cesium.Ellipsoid.WGS84.cartographicToCartesian(
|
||||||
@@ -698,6 +722,9 @@ function placeCamera(target, info, nowMs, dtSec, profile, look = null) {
|
|||||||
this.scratchCamera,
|
this.scratchCamera,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (info.generic === true) {
|
||||||
|
// Inside a structure there is no clearance to give back.
|
||||||
|
this.subjectLiftM = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewer.camera.setView({
|
this.viewer.camera.setView({
|
||||||
|
|||||||
@@ -406,10 +406,18 @@ test('the eye stands its height above the frame, and the ground clamp can be lif
|
|||||||
cockpit.update();
|
cockpit.update();
|
||||||
assert.equal(shots.length, before + 1);
|
assert.equal(shots.length, before + 1);
|
||||||
// Inside a structure the frame stands below the rendered floor: clamped,
|
// Inside a structure the frame stands below the rendered floor: clamped,
|
||||||
// the eye is pushed up to the floor; with groundClamp false it stays.
|
// the eye is pushed out of the floor at once and its clearance eases in;
|
||||||
|
// with groundClamp false it stays.
|
||||||
setFloor(FLOOR_M + 200);
|
setFloor(FLOOR_M + 200);
|
||||||
clock.advance(16);
|
clock.advance(16);
|
||||||
cockpit.update();
|
cockpit.update();
|
||||||
|
const lifted = shotHeight(shots);
|
||||||
|
assert.ok(lifted >= FLOOR_M + 200 - 0.01, 'out of the floor at once');
|
||||||
|
assert.ok(lifted < FLOOR_M + 200 + 1.5 - 0.01, 'the clearance eases in');
|
||||||
|
for (let i = 0; i < 80; i += 1) {
|
||||||
|
clock.advance(16);
|
||||||
|
cockpit.update();
|
||||||
|
}
|
||||||
assert.ok(shotHeight(shots) >= FLOOR_M + 200 + 1.5 - 0.01, 'clamped');
|
assert.ok(shotHeight(shots) >= FLOOR_M + 200 + 1.5 - 0.01, 'clamped');
|
||||||
ride.subject.frame = { ...ride.subject.frame, groundClamp: false };
|
ride.subject.frame = { ...ride.subject.frame, groundClamp: false };
|
||||||
clock.advance(16);
|
clock.advance(16);
|
||||||
|
|||||||
Reference in new issue
Block a user