From the whole-globe view the camera could end up facing empty space with
only stars on screen. Cesium's free look (Shift+drag) turns the camera in
place, and from 18,000 km the planet's edge is only about 15 degrees from
nadir, so a few drags turned it out of frame. The rolled pose was then
saved into the share hash, so a reload or shared link opened on stars too.
- Allow free look only below 1,000 km. Orbit, tilt and zoom are unchanged.
- Measure how much of the frame shows the planet's surface by casting a
32x18 grid of view rays at the WGS84 ellipsoid. While the scope mask is
on (the default) only the rays inside its keyhole count, since the mask
hides or dims the rest of the frame.
- When a camera move ends above 1,000 km with no ray on the surface, turn
straight down at the same position and heading. Limb glow and a sliver
up to about one grid cell thick can count as out of view. The end of a
move comes from Cesium's moveEnd and from a check of its own that
samples the pose every 250 ms after moveStart and acts once it has held
for half a second: at some of these poses the camera drifts by a
rounding error every frame, so Cesium never sees it stop and never
raises moveEnd. The zoom-out guard from #1522 keeps wheel and pinch
zoom-outs in view; this catches the other ways a move can end looking
away, such as a saved or shared view that did, or a
drag that Cesium turns into an in-place look below its 7,500 km
trackball height (one that starts on the stars, or a tilt drag with the
screen centre off the planet). Views with more surface, tracked or
locked cameras and the 2D and Columbus modes are left alone.
- Voice tilt and in-place turns of a free 3D globe camera above 1,000 km
stop when about a tenth of the frame still shows the planet (or about as
much as it showed when it started, two grid cells of sampling noise
allowed), never below about 1% of the visible frame; a view that already
shows less only keeps or gains what it has. A turn that cannot go half a
degree further without crossing that limit is refused with a reason
instead of reported as done, and a turn it accepts turns at least that
half degree. A turn that keeps or gains surface is not refused, even
after the window has been resized, except that near the limit, where the
planet's edge crosses the sample grid, sampling wobble can occasionally
refuse one (most often above about 15,000 km and on phone or ultrawide
frames); nothing then turns away from the planet. A turn that starts
where the last voice turn left the camera keeps that turn's limit, or a
higher one if the planet now fills more, so repeating the request does
not take any more; once anything else has moved or turned the camera, a
turn is measured from what is on screen. A request made while the camera
is tracked is judged by the pose voice's release of the follow camera
will leave, before that release, so a refused turn keeps the tracking.
The frame measure and the look-step limit live in src/globeInView.js,
shared by the viewer and the voice camera verbs.
Re-pin the Realtime tool payload hash for the merged tree (voice geometry plus pitchDeg). To fit main's 55,500 prompt ceiling with Overpass on, the pitchDeg schema drops its -90..90 range (the runner already clamps to -89..-5, now pinned by a test) and its description is shorter. The Overpass prompt measures 55,492.
Picks up #1527, which restores the 97 format-scope entries an earlier merge
dropped; this branch had kept the same 97. The resolved list is main's plus
this PR's 12 test and helper files, 1,222 paths, all present.
The guard's own tests pass even if createApplicationControls stops
installing it. This checks the ceiling and frame listeners are in place
after construction and gone after teardown.
Merge commit 34906c9a resolved the end-of-list conflict in scripts/format-scope.json with the branch's side, which dropped the 97 entries main had appended since Oct 26. Those files all still exist; 64 of them are tests that runtime discovery does not cover, so format:check had stopped checking them.
Each entry goes back at its position on main before the merge. All 1680 files pass npm run format:check.
Picks up #1308 (Overpass admission), #114 (Sea Marks and Ski Pistes),
#1206 (radio country coverage) and #1435. Resolutions:
- SECURITY.md: main's rewritten cross-site paragraph, with voice place
search listed beside /api/overpass among the keyless map routes.
- scripts/format-scope.json: both sides. A merge on main dropped 97 entries
(being restored there); keeping them, this PR's 12 and main's 2 new ones
keeps every file in format:check. All 1,222 paths exist.
- Translation catalogs: main's two new keys and this PR's 47, no overlap.
- Layer count 36 (33 + OSM Places + Sea Marks + Ski Pistes); both schema
pins re-derived, the first-run length is exactly 30,919 + 292 + 107.
Typed chat sessions no longer end on the voice inactivity timeout, and a
failed or stopped reply keeps the conversation: Stop cancels only the
reply in progress, and map actions that already ran stay in the model's
context. Each upstream request and map action gets its own deadline, old
tool results shrink instead of the history overflowing, and step limits,
a missing or rejected key, the OAuth lock and throttling each get their
own message.
- Server: OpenAI chat is off under GEV_PREFER_CODEX_OAUTH; chat throttles
default to the provider's throttle; replies carry API_KEY_MISSING,
API_KEY_REJECTED, CODEX_OAUTH_REQUIRED and RATE_LIMITED codes; upstream
Retry-After passes through, and a rejected key (Gemini's invalid-key 400
included) cools down for a minute
- Pinokio forwards the chat throttle settings
- UI: the reply reveal publishes only the message list; the border orbit
runs only while busy; layers attach with a button or Enter; the log is
aria-busy while a reply types out; auto-scroll respects a reader who
scrolled up; Escape closes the attach menu; the chat gear says Settings;
a note says chat bills the OpenAI key when voice uses ChatGPT sign-in
- Layout does less work while panel height animates; unused blurs removed
- Gemini voice start ends chat; the voice preview clears on every new
session
- Tests for the route gates, client limits, attachments and the fixes
Review fixes for #1506.
- Ottawa: use the documented camera list and the city's Traffic Camera
Images API. The pack is skipped with no request unless
OTTAWA_CAMERA_CERTIFICATE is set; the certificate is added only at
fetch time inside the pinned /opendata/ path, and each camera is
fetched at most once a minute through the shared frame cache.
- Québec 511 video is opt-in (CCTV_QUEBEC_VIDEO_ENABLED=1) and loads
only after the viewer presses Play; selection, auto-hop, scenes and
voice no longer contact Québec 511. Credit splits locations (MTQ,
CC BY 4.0) from video (Québec 511). Interrupted play() is no longer
reported as an unavailable source; the per-frame listener runs only
while a Québec camera is selected and the canvas rect is cached.
- Catalog cap: reserved Québec and World slots come only from capacity
above the 4,000 shared slots, so a lower CCTV_MAX_SOURCES thins every
pack instead of starving the existing ones. dev-fresh and
.env.example default to 7,516.
- Throttled frames (World, Ottawa): inside the refresh floor with no
cached frame the proxy serves the placeholder, never Street View; a
failed fetch backs off for a minute; the cache is bounded by bytes.
- MJPEG first-frame extraction runs only for sources registered as
MJPEG (Taiwan Highway Bureau), caps a frame at 2 MB, reads in linear
time, accepts a CRLF preamble and a boundary that already starts with
"--", and ends a part without Content-Length at the next delimiter.
THB hosts are registered in the redirect pin list.
- Tests: World loader policy, Québec loader and opt-in, proxy cooldown
without Street View, Ottawa signing, cap reservations at 7,516, 4,000
and 48, overlay Play gate, refresh, visibility and teardown. Guard
tests check the 4,000 shared slots again.
- Remove the unused standalone Québec preview module and public page.
- Docs: DATA_SOURCES, SECURITY, CURRENT-STATE, CCTV_INTEGRATION, README,
.env.example, changelog fragment renamed to its full scope.
From review:
- The browser client for /api/osm/features gets its own tests (request
shape, abort, network failure, coded refusals), and a 200 whose body is
not JSON is now a failure instead of zero places.
- An end-to-end test runs resolve_area, osm_query and an analyst follow-up
through the real runner and checks the follow-up keeps the area; it fails
with the wiring reverted.
- The Realtime token test pins Overpass off and compares the full tool
schemas again instead of names taken from the environment.
Cesium zooms a shallow view out by backing along the cursor ray with
heading and pitch held fixed, and GEV set no zoom ceiling. Past ~700 km
the limb drops below the fixed pitch, the steps grow with height, and
the camera flies off into space.
Cap user zoom at 40,000 km (whole-Earth view; flights are not limited)
and, above 300 km during a user wheel, pinch or right-drag zoom-out,
ease the pitch down to at least 8 degrees below the Earth's limb. The
clamp yields to disabled inputs, tracked transforms and camera flights.
The Cockpit Contact counts named fixed providers (OpenSky, adsb.lol,
AISStream) while the Layers panel and Contacts named the provider the last
snapshot reported, and the vessels layer kept that name out of getStats(),
so Contacts always said AISStream. Each cohort now carries the provider and
whether it answered as a fallback or from a stale snapshot, and both views
show it the way the Layers row does ("adsb.lol · FALLBACK").
Fixes HAL-721.
A voice fly-to frames a city top-down, at pitch -90. The tilt clamp
rejected any step that left the pitch below -89, so the first frame of
"tilt up" stopped the motion and "tilt down" was refused: from the view
voice lands on, no tilt ever moved the camera. A bare "tilt the camera"
also failed because tilt required a direction.
- Clamp tilt only in the direction of travel, so -90 can tilt up.
- Swing the camera about the ground under the crosshair, so the place
stays centred and the heading is kept. A tracked view still tilts in
place, and a pivot tilt yields when tracking takes the camera.
- Add an optional move_camera pitchDeg for "tilt 45 degrees"; a tilt
with neither angle nor direction settles at -45 (oblique view).
- Re-derive the voice tool schema pins for the new argument and wording,
and keep the Realtime prompt under its 52,000-character budget.
Picks up the Radio Next/Previous fix (#1474) and the Codespaces docs
(#1515). The one conflict is the Radio cycle assertion in
gevActions.test.mjs: main's new fallback flag is kept in this branch's
layout.
Pulses: three dim rings grow from the Global Context subject to the
250 km range ring at constant speed, one every 3 s, each taking 9 s.
Each fades in over its first 8% of travel, then fades as
(1 - travel)^1.5 to about a third of its 60% peak, so it arrives at the
ring dim but visible. They are 3 px in the ring's colours, as polylines
with CallbackProperty positions and colours that share the ring's
centre object, so they follow a moving subject.
Ring: while pulses run, the 250 km ring rests unseen and flashes to full
strength as each pulse reaches it, fading over 1.8 s. Without pulses
(reduced motion, or a scene with no primitives or preRender hook) it
stays on screen as before.
Gradient: behind each pulse a radial band in the same blue fades inward
over 35% of the radius. One flat, translucent, unpickable 250 km disc
draws it with a small Fabric material. A preRender listener writes the
pulse radii and opacities into two vec4 uniforms each frame. The disc
(0.25 degree granularity, ~3.5 ms to build) is rebuilt only after the
subject moves 2 km.
Echoes: as a pulse front crosses a flight, military aircraft or AIS
vessel, the detection overlay draws a second bracket in the pulse blue
around that contact's own bracket, spreading outward as it fades over
1.4 s. Awareness publishes the field (centre, radius, pulse timing,
echo styling) through the new src/awarenessEcho.js. The overlay reads
it, so no layer's icon or colour is touched. Mapped installations have
no detection bracket, so they do not echo.
Rendering cost: pulses never hold continuous rendering. They ask for a
frame every 80 ms, the Cyber sonar's parked cadence, so a parked
Contacts view repaints at ~12.5 fps while a subject is shown. This
changes the "parked Contacts stays quiet" behaviour on purpose.
clearVisual stops the timer and removes the disc, the listener and the
echo field.
awarenessRangeRingPositions can now reuse a result array, and passes the
WGS84 ellipsoid explicitly so a reused Cartesian never lands in the
ellipsoid argument. Tuning lives in AWARENESS_RANGE_PULSE,
AWARENESS_RANGE_ECHO and AWARENESS_RANGE_RING_STYLE. src/awarenessEcho.js
is registered in the awareness-layer, application-components and
application-layer-construction packages.
Tests cover:
- the pulse curve and calm rate;
- the ring flash once per arrival, and the static ring without pulses;
- the 80 ms frame cadence and its stop;
- the partial-scene fallback;
- the gradient disc: unpickable, uniform values, rebuild and cleanup;
- per-frame evaluation, and the echo field following the subject;
- awarenessEcho strength, rim wrap, layer filter and altitude handling;
- the detection renderer's echo-bracket band and outward spread.
The Global Context range ring was a Cesium ellipse outline at 1 px in
rgba(98,181,255,0.72). WebGL ignores larger ellipse outline widths, so it
could not be thickened, and it vanished over desert and snow and under the
FLIR, NVG and CRT styles.
Draw it as a polyline instead: a 4 px line with a 2 px Context-blue core
inside a 1 px dark halo on each side (PolylineOutlineMaterialProperty).
The halo gives luminance contrast on light and dark ground, which the
monochrome styles keep. The ring sits at the same 2.5 km height and
follows the subject on the same 250 m threshold. Positions come from a
new awarenessRangeRingPositions helper that stays within 50 m of 250 km.
Style values live in AWARENESS_RANGE_RING_STYLE (awareness/policy.js).
Adds rangeRing.test.mjs (geometry at equator, Sahara, Greenland and the
antimeridian; contrast; entity shape; follow threshold) and a
CURRENT-STATE note.
- strip control and format characters from OSM tag values, keeping the
zero-width (non-)joiners Persian and Indic names spell with, and cut
them at 120 code points so a name cannot steer or flood the model
- adopt the nine new geometry test files in format-scope.json; five of
them change by formatting only
Both stack parents (#837, #842) are on main, so the voice geometry branch
now builds on main directly. Conflict resolutions keep main's behaviour and
add the geometry work on top: osm_query gating on every provider, restored
admin identity for Nominatim outlines, merged analyst follow-up memory, and
the cancelled Globe View reset instruction ahead of the generic cancellation.
Resolve the CURRENT-STATE.md layer table by keeping main's realigned
table and adding this layer's row. Main added an Arabic locale, so
translate the 12 layer strings and list ar.json in the drone-airspace
package.