mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 06:42:23 +00:00
Merge pull request #1694 from bilawalsidhu/sameh/cctv-globe-plane-clamp
fix(cctv): on a globe map the camera plane ends where the view meets the ground
This commit is contained in:
6 files changed
+72
-7
No files matched your search
@@ -0,0 +1,3 @@
|
||||
- End a CCTV camera's monitor plane where its view meets the ground on globe
|
||||
maps too, instead of a kilometre-wide plane hundreds of metres up
|
||||
(samehkhamis, #1694).
|
||||
@@ -6572,6 +6572,10 @@ silently demoting every later lookup for the session.
|
||||
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —
|
||||
auto-calibration and the drape mesh pipeline are deleted. A one-shot activation obstruction
|
||||
probe (`pickFromRay` on camera activation, clamping the plane short of the first hit) remains;
|
||||
a hit at or beyond the camera's range counts as a miss, and on a globe map the plane also clamps
|
||||
short of the globe's terrain along the axis (flat ground at the mount's ground when terrain is
|
||||
missed or met beyond the range), whichever is nearer, so it never stands at the full range there
|
||||
(about a kilometre wide and hundreds of metres up);
|
||||
ground placement is superseded by the shared-floor v3 behavior below. Calibration persists to
|
||||
`godsEyeView.cctv.calibration.v2` (wiped clean, no v1 import); a panel-only CAL badge shows
|
||||
`CALIBRATED`/`CURATED`/`RAW PRIOR` (no in-world tint). Panel is titled "CCTV" (not "CCTV
|
||||
|
||||
@@ -29,6 +29,7 @@ export const writeCalibrationStoreV2 = layer.writeCalibrationStoreV2;
|
||||
export const deriveCalBadge = layer.deriveCalBadge;
|
||||
export const computeFrustumGeometry = layer.computeFrustumGeometry;
|
||||
export const activationProbeClampRange = layer.activationProbeClampRange;
|
||||
export const flatGroundAxisDistance = layer.flatGroundAxisDistance;
|
||||
export const frameSignatureFromPixels = layer.frameSignatureFromPixels;
|
||||
export const _createCctvProjectionPlaneForTest =
|
||||
layer._createCctvProjectionPlaneForTest;
|
||||
|
||||
@@ -35,6 +35,7 @@ import cctvLayer, {
|
||||
_pushAmbientCardEntriesForTest,
|
||||
_setCctvOverlayHostForTest,
|
||||
activationProbeClampRange,
|
||||
flatGroundAxisDistance,
|
||||
bindCctvWorldClickGesture,
|
||||
clearProbeClampOnDeactivation,
|
||||
computeFrustumGeometry,
|
||||
@@ -655,6 +656,17 @@ test('activation obstruction clamp uses the field-derived 12 m floor', () => {
|
||||
assert.equal(activationProbeClampRange(210, 250), null);
|
||||
});
|
||||
|
||||
test('on a globe map the axis ends on flat ground at the mount ground', () => {
|
||||
// A 24 m mount pitched 30° down meets the ground 48 m along its axis.
|
||||
assert.ok(Math.abs(flatGroundAxisDistance(24, -30) - 48) < 1e-9);
|
||||
// The default 700 m range then clamps just short of that, not 700 m out.
|
||||
const clamp = activationProbeClampRange(700, flatGroundAxisDistance(24, -30));
|
||||
assert.ok(Math.abs(clamp - 44) < 1e-9);
|
||||
assert.equal(flatGroundAxisDistance(24, 0), null);
|
||||
assert.equal(flatGroundAxisDistance(24, 10), null);
|
||||
assert.equal(flatGroundAxisDistance(0, -30), null);
|
||||
});
|
||||
|
||||
test('deactivation clears the probe clamp and restores nominal frustum geometry', () => {
|
||||
const record = {
|
||||
camera: { ...UNCLAMPED_CAMERA },
|
||||
|
||||
@@ -260,6 +260,21 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
return Math.max(PROBE_MIN_RANGE_M, hitDistance - PROBE_CLEARANCE_M);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distance along a pitched-down view axis to flat ground at the mount's own
|
||||
* ground height, or null for an axis at or above the horizon.
|
||||
* @param {number} mountHeightM Mount height above its ground.
|
||||
* @param {number} pitchDeg Axis pitch (negative looks down).
|
||||
* @returns {number|null}
|
||||
*/
|
||||
|
||||
function flatGroundAxisDistance(mountHeightM, pitchDeg) {
|
||||
const height = Number(mountHeightM);
|
||||
const pitch = Number(pitchDeg);
|
||||
if (!Number.isFinite(height) || height <= 0 || !(pitch < 0)) return null;
|
||||
return height / Math.sin(Cesium.Math.toRadians(Math.min(89, -pitch)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a computeFrustumGeometry result into the Cartesian3 positions the
|
||||
* entities consume. Fresh objects per call (geometry updates are rare —
|
||||
@@ -616,10 +631,14 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
* range, clamp the plane's effective range just short of the first hit so the
|
||||
* "big and dramatic" true end cap never clips into downtown buildings.
|
||||
*
|
||||
* This is the ONLY raycast in the whole CCTV subsystem — once per activation,
|
||||
* never per-frame (the zero-raycast invariant applies to steady state). The
|
||||
* per-camera range slider overrides the clamp: a user-set rangeScale skips the
|
||||
* probe entirely. Probe failure/miss keeps the unclamped range.
|
||||
* On a globe map it also picks the globe's terrain along the same axis
|
||||
* (flat ground at the mount's ground when terrain is missed or met beyond
|
||||
* the range) and clamps at the nearer end. These are the ONLY raycasts in the
|
||||
* whole CCTV subsystem — once per activation, never per-frame (the
|
||||
* zero-raycast invariant applies to steady state). The per-camera range
|
||||
* slider overrides the clamp: a user-set rangeScale skips the probe
|
||||
* entirely. A miss on the photoreal stack or a probe failure keeps the
|
||||
* unclamped range.
|
||||
* @param {Object} record - Camera record being activated.
|
||||
*/
|
||||
|
||||
@@ -646,9 +665,33 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
for (const runtime of layerState._projectionEntities) {
|
||||
if (runtime?.planeEntity) exclude.push(runtime.planeEntity);
|
||||
}
|
||||
const hit = scene.pickFromRay(new Cesium.Ray(mountPos, dir), exclude);
|
||||
if (!hit?.position) return;
|
||||
const dist = Cesium.Cartesian3.distance(mountPos, hit.position);
|
||||
const ray = new Cesium.Ray(mountPos, dir);
|
||||
// pickFromRay hits only primitives (the photoreal tiles), never the
|
||||
// globe: on a globe map the ray runs on through the Earth and can meet
|
||||
// a primitive thousands of kilometres away, which blocks nothing. Such
|
||||
// a hit or none left the plane at the full nominal range, about a
|
||||
// kilometre wide and hundreds of metres up. A hit at or beyond the range
|
||||
// counts as none. On a globe map the axis also ends on the globe's
|
||||
// terrain, the nearer of that and any primitive hit (a primitive behind
|
||||
// a hill the axis has already entered blocks nothing). Terrain still
|
||||
// loading at a coarse level can be missed or met beyond the range; then
|
||||
// flat ground at the mount's own ground stands in for it.
|
||||
const inRange = (distance) =>
|
||||
Number.isFinite(distance) && distance > 0 && distance < camera.rangeM;
|
||||
const hit = scene.pickFromRay(ray, exclude)?.position;
|
||||
const hitDist = hit ? Cesium.Cartesian3.distance(mountPos, hit) : NaN;
|
||||
const distances = inRange(hitDist) ? [hitDist] : [];
|
||||
if (scene.globe?.show) {
|
||||
const terrain = scene.globe.pick(ray, scene);
|
||||
const terrainDist = terrain
|
||||
? Cesium.Cartesian3.distance(mountPos, terrain)
|
||||
: NaN;
|
||||
const ground = inRange(terrainDist)
|
||||
? terrainDist
|
||||
: flatGroundAxisDistance(camera.mountHeightM, camera.pitchDeg);
|
||||
if (Number.isFinite(ground) && ground > 0) distances.push(ground);
|
||||
}
|
||||
const dist = distances.length ? Math.min(...distances) : null;
|
||||
record.probeClampRangeM = activationProbeClampRange(camera.rangeM, dist);
|
||||
} catch {
|
||||
// probe failure → keep the unclamped range
|
||||
@@ -831,6 +874,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
hasShippedFootprint,
|
||||
footprintPose,
|
||||
activationProbeClampRange,
|
||||
flatGroundAxisDistance,
|
||||
frustumCartesians,
|
||||
frustumFrameEcef,
|
||||
applyFrustumGeometry,
|
||||
|
||||
@@ -73,6 +73,7 @@ export function createCctvLayer({ services, source }) {
|
||||
deriveCalBadge: parts.calibration.deriveCalBadge,
|
||||
computeFrustumGeometry: parts.geometry.computeFrustumGeometry,
|
||||
activationProbeClampRange: parts.geometry.activationProbeClampRange,
|
||||
flatGroundAxisDistance: parts.geometry.flatGroundAxisDistance,
|
||||
frameSignatureFromPixels: parts.frames.frameSignatureFromPixels,
|
||||
_createCctvProjectionPlaneForTest:
|
||||
parts.testing._createCctvProjectionPlaneForTest,
|
||||
|
||||
Reference in new issue
Block a user