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.
Opening Utilities while Data Layers is open no longer forgets that Contact
was folded for the first, and closing one of them leaves Contact folded
while the other is still open. Enter and Esc in a drawn shape's name keep
focus in the field instead of dropping it to the page. The draw tool's
destroy test now checks it stops following the board, and the Draw gate's
amber-count comment sits on its function again.
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
qa-draw-tool drives the rows, waits on a mark's state instead of fixed
sleeps, checks that fly-to centres the line, and compares two screenshots to
show a hidden Line's pixels go and come back. Its clicks and pixel clip stay
clear of the Utilities panel beside the left rail. qa-layer-panel checks
that Directions returns to the Utilities list after a reassembly.
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
Directions, the Contacts marker and panel, placed annotation labels, the
drawn-line measure, the Cockpit and formatRouteDistance each chose the unit
and precision on their own, and most chose before rounding: #1450, #1433 and
formatDistanceText or kilometreDigits from src/data/distanceFormat.js, which
cut after rounding. Each keeps its own metre step and rendering.
That also fixes the 10 km cut the Contacts marker and panel, the drawn-line
measure and the Cockpit still had (9,960 m read 10.0 km). The helper returns
kilometres already rounded, so the plain marker and the localized panel no
longer disagree on half-tenths such as 1,450 m.
A negative annotation distance now leaves the label without a distance, and
a drawn line with a coordinate that is not a number shows no measure, rather
than printing 0 m or NaN km. The HUD altitude tag and the spoken view height
keep their own unit choice.
Since the UI moved to single-file components, every unit-test process
compiled the views it imports from scratch, and the loader ran off-thread
(module.register), adding a round trip to every module a test imports,
including the ones that are not components. Core's unit tests took about
twice as long, and a host running Core's suite on smaller runners passed
its CI time limit.
register-vue now installs synchronous hooks on the loading thread
(module.registerHooks). Compiled components are kept in the system temp
directory, keyed by the file's path and text, the Vue version and the
loader's own code, so each one compiles once across test processes; the
compiler itself loads only on a cache miss. Cached modules run as code, so
the directory is used only when it is a real directory owned by the current
user and writable by no one else; on Windows it lives in the user's local
app data instead of a temp directory that may be shared. The cache never
sits inside the package tree, and GEV_VUE_CACHE=0 compiles every time.
On four cores the full suite went from 441 s to 240 s from a cold cache.
The transit, ALPR, vessel, CCTV and flight/military context fixes each
wrapped 359.5° to north with an inline Math.round(x) % 360. They now call
roundHeadingDegrees in src/data/headingDegrees.js, which also wraps negative
and multi-turn bearings. normalizeHeading moves there with it, and
cockpitMath re-exports it so existing imports keep working. It now adds a
turn only to a negative remainder, so a bearing just under half a degree
keeps its precision.
The awareness, Local ADS-B, cockpit wind, cockpit heading and Street Level
readouts use the same helper. The Cockpit's TRACK ACQUIRED signal rounded
without wrapping and could read COURSE 360°; it now uses
formatHeadingReadout, which shows --- for a track that is not finite.
The application package boundary owns src/i18n/locales/ar.json beside the
other catalogs, and the style.css shim test checks that the shim holds only
import statements rather than counting its lines, so adding rtl.css to it
does not read as rules.
Selects pad the chevron's side with padding-inline-end, as do the Radio
filter, the SDR gain select and the voice timeout select, which set their
own padding; rtl.css moves the chevron to the left, where an Arabic field's
text ends. A rail legend's range keeps left-to-right order under its colour
ramp, which always runs low to high from left to right.
Main moved the UI to Vue views and Pinia stores, so the Arabic changes to
the old DOM controllers now live in their views: the rail legend range
comes from formatScaleRange (keeping units a source supplies as given),
the voice timeout options are worded by Intl unit formatting in
VoiceControl, and the scene editor's JSON fields stay left to right in
SceneDialog. Main's CCTV status line, launch panel and calibration values
keep the Arabic isolates and Western-digit formatting.
isolate() and keepCodesWithValues() read the locale through getLocale(),
which follows the page's vue-i18n instance, and the Intl formatters take
intlLocale(getLocale()). A language switch now applies in place, and
applyDocumentLanguage sets <html dir> on every switch, so the page mirrors
without a reload.
The icon rails and their tips are placed with logical properties, so they
stay on the outer side of the mirrored stacks; Cyber's new stack offsets,
padding and edge lines are logical too, and rtl.css moves Cyber's right
stack to its new rail clearance. Main removed the Context mini radio, so its
right-to-left rules and test go.
The Arabic catalog gains main's new strings and drops the ones main no
longer uses; the other catalogs keep main's new keys and the branch's
landing labels.
Every data-i18n, data-i18n-title, data-i18n-aria-label and
data-i18n-placeholder marker in the Vue templates becomes a t() call with
the key the boot pass used to look up, contexts included. The three labels
the shell rewrote at boot (Visual Presets, Location and the standby
description) render through t() too, and that rewrite is gone. With the
locale in vue-i18n, view text now follows a locale change, and markup
under v-if or v-for no longer comes back in English when it re-renders.
Rendered text and attributes are unchanged in every locale. The boot pass
still translates static page markup such as the loading screen and a host's
own markup, and npm run i18n rejects data-i18n* in a Vue template.
The application views install one vue-i18n instance that holds the active
locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale()
keeps its locale in the instance's ref, so t(), tn() and the Intl formatters
read it from there and a view that translates while rendering renders again
when the locale changes. A tag written straight to the ref is normalized.
Message formatting, catalogs and call sites are unchanged, and switching
language still reloads the page.
The instance outlives the views: unmounting one no longer disposes it. The
browser build defines vue-i18n's feature flags and drops its message
compiler, which Core does not use.
VisualSettings publishes the active style's parameter rows to the display
store, and the Display panel renders them with the shared row component;
a moved slider goes back through the store's setStyleParameter action.
The readout snaps a value to its step as the browser does, half-steps up
and an off-grid maximum down. A preset that resets the shown style's
uniforms republishes its rows, input for another style's parameters is
ignored, and a stopped owner takes its rows with it unless a newer owner
has published since. createStyleParameters is deprecated and builds its
rows the same way.
RecordingControls publishes the safe frame a recording composes in, 16:9
or 9:16 (null while none records), to the new recording store, and the
scene chrome renders the overlay's active and ratio classes from it
instead of the controls writing them. Any frame but 9:16, even none named,
records in 16:9 as before.
The top-center and cockpit reset buttons call the location controls'
new reset() through the globe actions store, and render a reset under
way from it, instead of the location controls binding their clicks and
the navigation writing their labels. The store keeps each owner's reset
under way like its clear, and controls replaced mid-reset keep it on the
buttons. LocationControls no longer takes resetButtons.
The camera orientation controls publish whether the map is tilted and the
camera's whole-degree heading instead of writing the buttons' attributes,
and answer tilt and north-up through the globe actions store. The new
GlobeCompass renders the heading, so a rotating camera re-renders only the
compass. The store drops an unchanged camera state and keeps each clear
owner's busy state, so the buttons show the shell that answers them: a
shell replaced by a newer one, or disposed mid-clear, leaves the live
shell's state alone, and an older shell that answers again shows its own.
createOwners gains owns() and ownerOf().
The top-center bar is now GlobeActions.vue. The shell publishes whether a
clear is under way and answers the clear and share buttons through the
globe actions store, instead of binding listeners and writing attributes on
the buttons. A shell disposed mid-clear resets the busy state, so the next
shell's bar is not left busy. bindClearLayersControl stays exported and is
deprecated.
UiSlider merges a caller's own input, change and blur handlers with its
own, leaves the input alone on a null model, and gains unstyled for a
range drawn as the browser's own. The rail card setting, observation
timeline, imagery opacity, Street Level date and Bhote Koshi split and
progress ranges now render through it. The timeline and the setting
ranges put the handle back once their owner has answered, since a drag
that ends where the slider last rendered leaves its model unchanged.
The radio tuner keeps its own input: its owner moves the handle back to
the selected station without changing the slot.
UiBadge takes a tone and carries Core's gev-status-badge class, with
the density variants and colours left to its callers; the CCTV source and
calibration badges and the rail card badges render through it. UiEmpty takes
a title and hint, with the title as the line itself, since Core's
empty states are one line; the Scene shot list, mission roster and imagery
strip use it.
UiSelect renders the bare native select with v-model, options and
disabled. Options that carry their own attributes come through the slot, a
caller's own change handler still reaches the select, and hold never
rewrites a focused select and takes the model back on blur. Without hold, a
select takes its model back once its change has rendered, so a pick its
owner refuses goes back, as the raw value binding did.
The HUD layout, draw colour, RTL-SDR gain, radio station tag, voice
provider, voice timeout, scene and CCTV camera selects now render through
it.
UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
UiButton gains pressedClass, Core's class on a pressed button. The pressed
classes toggle through classList, so classes other code adds survive a
press, a caller's class change keeps them, a renamed pressedClass leaves
the button, and server-rendered markup carries them.
The Display, SDR, Radio and Cockpit enable, Cockpit weather and briefing,
mission replay, Street Level, TR-3B, overlay, voice and CCTV toggles now
render through it with the same ids, classes and aria-pressed values.
UiSlider takes v-model, min, max, step and disabled
over Core's native range input, emits numbers, and with `hold` never
rewrites a focused handle and takes the model back on blur. It is a
functional render function, so the portable style parameter rows use it
too. The Display, Radio, RTL-SDR, Cockpit, mission replay and style
parameter sliders render with it; server-rendered markup keeps their
default values.
The Radio panel's and Cockpit's Enable, launcher, station tag,
transport and volume controls call RadioControls through the radio
store instead of DOM listeners. A refused station tag gets the kept tag
back from act(), which now returns what the answering action returned.
The Cockpit's station steps keep rotate: false. The unused element
fields go from the shell.
SdrCard calls LocalSdrControls through radio store actions instead of
DOM listeners, and renders its gain options from the receiver's own
R820T gain steps with page-language labels. The display and radio
stores share one owner list (createOwners): the newest connected owner
with an action answers it.
Street Level's fit and panorama filters and Recent Imagery's mode render
through the kit's UiSegmented, and their owners answer the choice
through the Street Level store (setPano, setRenderMode) and the
readout's onSetMode. The imperative segmentedControl.js adapter and its
test go. UiSegmented moves focus at once on a key, a radio group from
the option holding focus and an arrows group from the selection, and
asks for a change only when the key lands somewhere new.
The file import no longer looks up or listens on the Import button, the
picker or the list, and no longer restores focus itself. It connects
importGeoFiles, toggleGeoImport, flyToGeoImport and removeGeoImport to
the display store. A new GeoImport view owns the button, the picker, the
rows and the hint: it opens the picker once the import has presented
itself, sends the pick and the row buttons' actions, and hands keyboard
focus on with v-focus-safe-children before a focused row goes, to the
next row's same button, else the previous row's, else Import.
The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.
Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
Cockpit presence now carries contextCollapsed and signalCollapsed.
setContextCollapsed and setSignalCollapsed update presence and republish
it only when the value changes, instead of writing the dataset and the
toggle attributes. CockpitContext and CockpitBriefing bind data-collapsed
and their toggles' aria-expanded, aria-label, title and icon from
presence. The Cockpit utility QA script waits a frame for Vue to render
before it reads the toggles.
A panel's collapsed, dock-pinned, dock-pinned-top, layout-auto-collapsed
and accordion-collapsed classes are drawn by its root's v-panel-state from
the gev-panel-chrome store. PanelChrome, its restore, the layout
controller, both rails and the side accordion present each panel's state
there instead of writing its classes; the rails call onCollapse(panel,
{collapsed, auto}). The directive applies a change in a synchronous
watcher, since the rails measure a panel in the task that opens or closes
it.
Destroying the chrome inside Cockpit now puts back the panels Cockpit's
entry closed while it can still present them. Without a shell, the voice
tools' set_panel_open reports that there is no panel to open. QA scripts
present the states they set instead of writing the classes.
PanelChrome no longer writes the panels' collapse buttons, the command
dock tray toggles' names or the dock pins' pressed state. It presents each
panel's open and pinned state to the new gev-panel-chrome store, and the
buttons, now views, call it there: PanelCollapseButton for the panel
headers, bound toggles and pins in the command dock. Clicks take effect
once the chrome is initialized; a new owner starts from its own states.
bindPanelDisclosure binds only a panel's Escape and refuses the retired
buttons and onChange arguments. UiButton joins the component kit.
SceneControls no longer takes element handles or writes the Scene panel's
button states, PLAY label, status line, progress bar or runtime chip: it
presents them to the gev-scenes store, and the panel's buttons, scene
select and IMPORT chooser call its actions there. The chooser opens only
while an owner presents the panel and is cleared once its import settles.
On destroy the panel keeps its last status and buttons, without shots or
the runtime chip, as before.
initFirstRunExperience no longer writes the launcher card's classes,
attributes and text, wires its listeners or removes it: it presents the
card's visibility, busy state, status line, copy and scroll fade to the
gev-first-run store and connects the controls the welcome view's choices,
DISMISS and "Don't show this again" call. A refused save hands the tick
back to the view to revert. The launcher still reads the rendered card for
its hit test, keyboard trap, focus and overflow measurement.
The owner-chosen copy moves to src/firstRunCopy.js, shared by both. The
source-text tests give way to component tests, and the first-run mutation
QA follows the new code (67/67 caught).
A weather source can describe its own products (credit, units, coverage,
legend) through an optional `display` object in each product manifest:
source, credit, units, coverage, info, legend and extent. The weather
layer uses each valid field in the row summary, legend, info text,
coverage navigation, outside-coverage status, layer stats and imagery
credit, and keeps today's built-in value for any field that is absent or
invalid. Units and legend describe one scale, so they apply only as a
valid pair; otherwise both keep their built-in values, and the radar
info heading names the units in use.
Text is single-line and length-capped. It rejects control characters
(U+0000-U+001F, U+007F-U+009F), line and paragraph separators, and
invisible or format characters: soft hyphen (U+00AD), combining grapheme
joiner (U+034F), Arabic letter mark (U+061C), Mongolian vowel separator
(U+180E), zero-width and direction marks (U+200B-U+200F), bidi embedding
and override (U+202A-U+202E), word joiner, invisible operators and bidi
isolates (U+2060-U+2069), variation selectors (U+FE00-U+FE0F), the
byte-order mark (U+FEFF) and tag characters (U+E0000-U+E007F).
Legends need 2-16 stops whose labels are plain decimal numbers or short
text and whose colours are #rrggbb. A number label must read back
exactly as the legend shows it: within the safe integer range, with no
leading zeros and no digits lost to rounding. Extents must be finite, in
range and non-empty. Supplied source and units text is data: the layer
marks it so the panels show it untranslated, while built-in values are
translated as before.
The imagery credit is escaped as plain text and follows a refreshed
manifest even when the shown frame stays the same. A refresh changes
only the credit: the shown frame keeps the snapshot it was staged from,
so a later host switch restages it with its own time and bounds and the
refreshed credit. Manifests without `display`, including the bundled
nowCOAST ones, render exactly as before.
IntelHUD no longer builds #intel-hud with innerHTML or writes its readouts
by id: it publishes its readouts, typing summary, REC blink, visibility,
layout variant and style colors through its publish option to the gev-hud
store, and the intel-hud application view renders them. The corners render
from mount so the panel layout controllers register them as obstacles.
The vessel layer presents the selected vessel's readout through
src/hudReadouts.js, which IntelHUD follows, instead of writing into the
HUD. The Cyber sonar state answers a HUD toggle from IntelHUD's visibility
rather than the class the view renders on its next update.
The awareness layer presents the compass ring, heading, cardinal labels and
contact bearing markers through subscribeDirectionOverlay(); the application
shell publishes them to the gev-awareness-direction store and the scene
chrome renders them in the viewer's container.
The world overlay presents its focusable mirror of painted cards through
subscribeWorldOverlayActions() and answers activateWorldOverlayAction();
the application shell publishes the list to a store and the scene chrome
renders the buttons and the status, announcing a repeated focus again.
The module stays free of Vue for the portable transit entry, and its
canvases remain painting surfaces.