plugin-designs: neutral comments in shared/ownerless maps
Rewrite code comments across doxa-research-map, upg-neighbor, research-map-plus-grid, rudder-voice, people-groups-grid, big-picture, prayer-walk-planner and all-people-groups to drop internal role names from measurement and provenance notes, keeping each technical fact. Comments only; behaviour unchanged.
This commit is contained in:
parent
35c68048e8
commit
06a48a9b42
17 changed files with 67 additions and 67 deletions
|
|
@ -194,7 +194,7 @@ const mapData = useMapData({
|
|||
// all 12,379 features, 3.7 MB of GeoJSON built and re-tiled TWICE (first paint, then the DOXA recolour);
|
||||
// slim, it is 1.25 MB, built once. The popup reads the row by id. The DOXA recolour no longer rebuilds
|
||||
// the 12,379 with all their fields: DOXA pins are a small source of their own drawn on top, and the base
|
||||
// is re-sent once, slim, WITHOUT them (an invisible twin per DOXA group still cost a draw: rega measured).
|
||||
// is re-sent once, slim, WITHOUT them (an invisible twin per DOXA group still cost a draw: measured).
|
||||
let rows = [] // the decoded rows; a feature's id is its index here
|
||||
let doxaIds = new Set() // row indexes in the DOXA set (empty until the prayer API answers)
|
||||
let doxaBySlugRop = new Map() // rop3 → { slug, name } for the pray link
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ watch(textDir, reflectDir)
|
|||
|
||||
// The host's people-group LIST (library/lists/), handed to both children as the only key of their
|
||||
// profile-config, so nothing else about them changes. Absent = the default list.
|
||||
// ★ THE LOCALE TRAVELS WITH THE LIST (rega, row 47: big-picture rendered 31 Arabic text nodes
|
||||
// ★ THE LOCALE TRAVELS WITH THE LIST (measured: big-picture rendered 31 Arabic text nodes
|
||||
// through profile-config against 79 through ?lang=, and check:host-locale-contract measured 402
|
||||
// against 888). This handed each child ONLY the list, so the one documented channel a partner has
|
||||
// for asking a nested element to speak Arabic — its profile-config — arrived empty of any locale.
|
||||
|
|
@ -155,7 +155,7 @@ watch(textDir, reflectDir)
|
|||
// Threading it costs nothing and is what ProfileLoader would have done had one existed beneath this
|
||||
// composite, which is exactly the (C) failure mode: a composite that BAKES its children in has no
|
||||
// ProfileLoader under it, so it must do that job itself.
|
||||
// This is ONE identified gap, not a claim to have closed the whole 402/888 difference; rega
|
||||
// This is ONE identified gap, not a claim to have closed the whole 402/888 difference; a later pass
|
||||
// re-measures.
|
||||
const listConfig = computed(() => {
|
||||
const cfg = {}
|
||||
|
|
|
|||
|
|
@ -529,7 +529,7 @@ const legendTopVar = computed(() => {
|
|||
// Languages tabs it reserved 56px for a toolbar it never draws, and only those two tabs, because
|
||||
// they are exactly the set showsFlyTools() returns true for. Every other tab took the 14px branch
|
||||
// and looked right, which is why it was reported as two tabs misbehaving.
|
||||
// MEASURED by rega at 1440x900 on main e1d0bc20: the band from the map's top edge (y 44) to the
|
||||
// MEASURED at 1440x900 on main e1d0bc20: the band from the map's top edge (y 44) to the
|
||||
// legend card (y 100) is 56px of EMPTY map on both tabs, with no LegendTools anywhere in the strip.
|
||||
// So the gap was dead space, not a toolbar the reader had not registered.
|
||||
const toolsShown = showLegendTools.value && showsFlyTools(activeLegendType?.value ?? activeLegendType)
|
||||
|
|
@ -698,7 +698,7 @@ watch(() => activeTabId?.value, async (newTabId, oldTabId) => {
|
|||
// ★ SCROLL THE TAB BAR, NOT THE PAGE. This was
|
||||
// btn.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' })
|
||||
// and 'nearest' does not mean "do not scroll the page" — it means "scroll every scrollable
|
||||
// ancestor the least amount needed", and the HOST PAGE is an ancestor. MEASURED by rega's embed
|
||||
// ancestor the least amount needed", and the HOST PAGE is an ancestor. MEASURED in the embed
|
||||
// sweep: a partner page carrying this map below the fold jumps to y≈640 the moment it mounts, on
|
||||
// a phone, where Simple Research is the default tab. The reader asked for nothing; the page moved
|
||||
// under them.
|
||||
|
|
@ -788,10 +788,10 @@ const mapData = useMapData({
|
|||
// then force it visible (country search stays on the current tab — d7e741b — so
|
||||
// the polygon layers, normally hidden off the Regions tab, must be shown here).
|
||||
// ★ EVERY region paint write goes through this, or the pre-warm above is undone the first time a
|
||||
// highlight is cleared. Found by cross-reading exp-shepherd's country-posters port (73438b43b), which
|
||||
// highlight is cleared. Found by cross-reading the country-posters port (73438b43b), which
|
||||
// wraps all eighteen of its clears while mine still wrote nine RAW constants — a raw value makes the
|
||||
// property constant again, so the next highlight recompiles the shader variant the pre-warm exists to
|
||||
// have already compiled. rega measured 0 ms on a fresh tab; a reader who clears a selection first was
|
||||
// have already compiled. Measurement showed 0 ms on a fresh tab; a reader who clears a selection first was
|
||||
// paying it again, which no run so far has exercised.
|
||||
// regionIdle now comes from the library (useLegendMapBridge), which already called it — this file
|
||||
// was defining a copy and then passing it in. See check:region-paint-single-source.
|
||||
|
|
@ -932,7 +932,7 @@ const legend = useLegendData({
|
|||
// ★ REMOVED: provide('legendRows', legend.rows). useLegendData returns `items`, never `rows`, so
|
||||
// this provided undefined from the day it was written, and nothing in the repo injects 'legendRows'
|
||||
// — LegendDesktop and LegendMobile read the `legend` key instead, which is what the comment above
|
||||
// used to claim was happening. exp-shepherd found the same line in countries-map, where it was not
|
||||
// used to claim was happening. the same line was found in countries-map, where it was not
|
||||
// merely dead: it made Export poster throw (NE-17, their d6916e05e). A provide that is undefined and
|
||||
// a comment that describes a contract nobody implements are worse together than either alone, so
|
||||
// both go. If a consumer ever needs these rows, it should provide legend.items and say so then.
|
||||
|
|
@ -1117,14 +1117,14 @@ provide('getActivePinColor', (properties) => {
|
|||
// already (fam:/lang:/dial: + region:/block:/country: + bloc:/cluster:/pg:/pgic:),
|
||||
// so this returns null for them and zoomToLegendRow falls back to node.filter.
|
||||
// ─── ONE WRITER FOR THE REGION POLYGONS ──────────────────────────────────────
|
||||
// ★ MEASURED (rega, main 373b7979b): a single WAGF row click produced NINE regions-* paint writes —
|
||||
// ★ MEASURED (main 373b7979b): a single WAGF row click produced NINE regions-* paint writes —
|
||||
// fill-opacity, line-color and line-width, three times each inside ~110 ms — because two paths painted
|
||||
// the same highlight from DIFFERENT sources: onSemanticTreeSelect from node.isoCodes, applyDimFilter
|
||||
// from mapLayers.regionIsoMap, each triggering the other through mapStore. Two sources for one visual
|
||||
// is not just wasted work; it is how they drift, and it already cost a debugging cycle when we had to
|
||||
// establish WHICH of them had cleared the highlight.
|
||||
//
|
||||
// ★ AND THE IDLE STATE IS DATA-DRIVEN ON PURPOSE. rega measured the row click compiling a new shader
|
||||
// ★ AND THE IDLE STATE IS DATA-DRIVEN ON PURPOSE. Measurement showed the row click compiling a new shader
|
||||
// variant: 5.5 s on MapLibre, 11.1 s on Mapbox (SwiftShader), because that click was the first moment
|
||||
// these three properties stopped being constants. Same defect as the pins, same fix: the idle value is
|
||||
// ['case', ['has', 'iso_3166_1_alpha_3'], x, x] — both branches identical, so not one pixel changes —
|
||||
|
|
@ -1143,7 +1143,7 @@ function prewarmRegionPaint(m) {
|
|||
// Was a THIRD local copy of the idle expression, under a different name — byte-identical body.
|
||||
// The library's regionIdle replaces it exactly.
|
||||
const wrap = regionIdle
|
||||
// ★ SKIP WHAT IS ALREADY DATA-DRIVEN, and note the test is not "is it an array". exp-shepherd wrote
|
||||
// ★ SKIP WHAT IS ALREADY DATA-DRIVEN, and note the test is not "is it an array". The country-posters port wrote
|
||||
// their copy to skip any array, reasoning that an expression is already data-driven so there is no
|
||||
// variant to warm. That is right for this layer — line-color arrives as a match on a feature property
|
||||
// — but it is right by accident: a ZOOM expression like ['interpolate',['linear'],['zoom'],…] is also
|
||||
|
|
@ -1163,10 +1163,10 @@ function prewarmRegionPaint(m) {
|
|||
// exactly that reverse. Mapbox then classifies the property as composite and reaches for an
|
||||
// interpolation factor the expression cannot supply — the class of
|
||||
// this.expression.interpolationFactor is not a function
|
||||
// rega saw once on Mapbox at L54, on the clear → other-row path. A missed pre-warm costs a frame; an
|
||||
// a run saw it once on Mapbox at L54, on the clear → other-row path. A missed pre-warm costs a frame; an
|
||||
// illegal paint costs the layer, so when the two rules disagree this one wins.
|
||||
//
|
||||
// exp-shepherd's port skips ANY array and is immune to this by accident; the difference between us
|
||||
// the country-posters port skips ANY array and is immune to this by accident; the difference between us
|
||||
// was never about zoom expressions being data-driven, it was about which mistake is recoverable.
|
||||
const readsZoom = (e) => Array.isArray(e) && (e[0] === 'zoom' || e.some(readsZoom))
|
||||
const warm = (layer, prop) => {
|
||||
|
|
@ -1185,9 +1185,9 @@ function prewarmRegionPaint(m) {
|
|||
|
||||
function paintRegionScope(m, isoCodes) {
|
||||
if (!m || !m.getLayer('regions-fill')) return
|
||||
// ★ CANONICAL FIRST, OR THE COMPARE BELOW IS A LIE (rega, L43: still 9 writes per click, not 3).
|
||||
// ★ CANONICAL FIRST, OR THE COMPARE BELOW IS A LIE (measured at L43: still 9 writes per click, not 3).
|
||||
// The two sources build the same region from different lists — node.isoCodes and
|
||||
// mapLayers.regionIsoMap[slug] — and they do not agree on ORDER. rega had already told me that in
|
||||
// mapLayers.regionIsoMap[slug] — and they do not agree on ORDER. The measurement had already shown that in
|
||||
// the very first stack trace ("a case over ['IND',…]" then "a case over ['MYS',…], a different
|
||||
// literal order") and I wrote a JSON-string compare anyway, which can never match two orderings of
|
||||
// the same set. Sorting and de-duplicating makes the expression a function of the SET, which is what
|
||||
|
|
@ -1755,7 +1755,7 @@ async function ensureRegionsLoaded() {
|
|||
if (regionsData && map.value) {
|
||||
mapLayers.addRegionsLayer?.(regionsData, 'doxa-regions')
|
||||
// ★ PRE-WARM HERE, which is the only moment that helps. These layers are added on first visit
|
||||
// to the Regions tab, and rega measured the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
||||
// to the Regions tab, and measurement showed the first ROW click paying 5.5 s (MapLibre) / 11.1 s
|
||||
// (Mapbox) of shader compile because that click was when their paint first became data-driven.
|
||||
// Making the paint data-driven at layer-add moves that compile here, where a 6.4 MB GeoJSON is
|
||||
// already loading, instead of into the middle of a gesture.
|
||||
|
|
@ -1904,7 +1904,7 @@ function pinBaseAlpha() {
|
|||
// matchExpr: an expression to dim by, null to clear dimming, undefined to re-apply what is current
|
||||
// (which is what the slider's watcher needs — it changes the scale, not the selection).
|
||||
// ★ THE UNDIMMED VALUE IS DATA-DRIVEN TOO, AND THAT IS THE WHOLE POINT (reported bug: "the research map
|
||||
// isn't connecting with the legend"; rega's CPU profile, 2026-09-28). A legend click on a big affinity
|
||||
// isn't connecting with the legend"; a CPU profile). A legend click on a big affinity
|
||||
// row appeared to do nothing for ~2 s. It was not the echo guard, not the legend's DOM (34 rows before,
|
||||
// 34 after) and not expression evaluation (the JS filter scan over 2,106 features against a 700-id list
|
||||
// measures 14 ms). It was ONE task of 3.2-3.5 s whose self time was getShaderParameter + getProgram-
|
||||
|
|
@ -1918,7 +1918,7 @@ function pinBaseAlpha() {
|
|||
// and the program compiles during boot alongside every other one. A selection then only re-uploads
|
||||
// attribute values, which is the cheap path.
|
||||
//
|
||||
// The trade, stated: boot compiles one more program (tens of ms on a real GPU; rega measured seconds
|
||||
// The trade, stated: boot compiles one more program (tens of ms on a real GPU; measurement showed seconds
|
||||
// only because that box rasterises on the CPU via SwiftShader). Paying it at load beats freezing the
|
||||
// map mid-gesture, and the variant was going to be compiled on first interaction anyway.
|
||||
function applyPinOpacity(m, matchExpr) {
|
||||
|
|
@ -1928,7 +1928,7 @@ function applyPinOpacity(m, matchExpr) {
|
|||
// ['has','uniqueId'] reads a feature property, which is what makes it data-driven; both outcomes are
|
||||
// `a`, so a pin without the property renders exactly as before.
|
||||
const value = _pinMatchExpr ? ['case', _pinMatchExpr, a, 0] : ['case', ['has', 'uniqueId'], a, a]
|
||||
// ★ ONE WRITE PER DISTINCT VALUE. rega counted circle-opacity rewritten 4-8 times for a single click
|
||||
// ★ ONE WRITE PER DISTINCT VALUE. Measurement counted circle-opacity rewritten 4-8 times for a single click
|
||||
// (+1.52, +1.57, +1.94, +5.17 s): the selection handler, the glow sync and the slider watcher all end
|
||||
// up here, and each write re-uploads the per-feature attribute. Every writer already routes through
|
||||
// this function, so the collapse belongs here and nowhere else. Compared as text because the value is
|
||||
|
|
@ -2325,7 +2325,7 @@ watch(() => profileConfig?.value?.externalFilters, (raw) => {
|
|||
try {
|
||||
// ★ AN EMPTY VALUE THE HOST DID NOT SET MUST NOT CLEAR WHAT THIS PAYLOAD JUST LIT. This is the
|
||||
// The report "the research map isn't connecting with the legend" on the WAGF Regions tab, found by
|
||||
// rega's stack trace (l40): the highlight painted at +51 ms and was wiped at +438 ms by
|
||||
// a stack trace (l40): the highlight painted at +51 ms and was wiped at +438 ms by
|
||||
// clearAllHighlights, reached from applyDimFilter({kind:null}) through the selectedRegion watcher —
|
||||
// so selectedRegion had gone FALSY while externalRegion still read "asia".
|
||||
//
|
||||
|
|
@ -2426,7 +2426,7 @@ watch(() => profileConfig?.value?.externalFilters, (raw) => {
|
|||
// ★ EACH HOOK IS ALSO HELD LOCALLY, so the unmount path can release ONLY WHAT IS STILL OURS.
|
||||
// Two of these maps can share a page (research-map-plus-grid composes one beside a grid, and the
|
||||
// gallery can show another): B mounts and overwrites A's hooks, then A unmounts. An unconditional
|
||||
// delete would strip the hooks belonging to the map still on screen, and rega would report a verify
|
||||
// delete would strip the hooks belonging to the map still on screen, and it would report a verify
|
||||
// hook missing on a map that is perfectly healthy — a false failure costing more to chase than this
|
||||
// costs to write. Declared out here rather than inside the block below because a const inside it is
|
||||
// invisible to onBeforeUnmount. Same pattern gospel used in d3597f3d1.
|
||||
|
|
@ -2534,7 +2534,7 @@ function emitFilterState() {
|
|||
//
|
||||
// The NAME still goes out because this map's own legend selects by name; the CODE goes out for
|
||||
// anyone matching rows. Both halves get the form they can actually use.
|
||||
// ★ THE LOOKUP HAS TO MATCH THE WAY THE LEGEND BUILT THE LABEL (rega, 2026-09-26, main 49992a5d:
|
||||
// ★ THE LOOKUP HAS TO MATCH THE WAY THE LEGEND BUILT THE LABEL (measured on main 49992a5d:
|
||||
// MapLibre, BOTH locales, Languages > Language > "Arabic" -> languageCode:"" -> grid 2,107 -> 0).
|
||||
//
|
||||
// WHAT I GOT WRONG. This found a row whose language LABEL equals the selected name with `===`.
|
||||
|
|
@ -2543,7 +2543,7 @@ function emitFilterState() {
|
|||
// splitting the API label at the comma. MEASURED against the live source (2,107 rows, all 2,107
|
||||
// carrying primary_language as {value,label}): there is NO row whose label is "Arabic". What
|
||||
// exists is "Arabic, Sudanese", "Arabic, Levantine", "Arabic, Gulf", ... So the exact match could
|
||||
// never hit, in any locale — which is why rega saw it fail identically in en and ar, and why the
|
||||
// never hit, in any locale — which is why it failed identically in en and ar, and why the
|
||||
// locale fix that prompted this code was right about the DISEASE and wrong about this line.
|
||||
//
|
||||
// This is the derived-value trap, one tier down from where I last wrote it up: a value that
|
||||
|
|
@ -2586,7 +2586,7 @@ function emitFilterState() {
|
|||
const detail = {
|
||||
region: mapStore?.selectedRegion || '',
|
||||
religion: religionName,
|
||||
// ★ A DIALECT'S NAME COMES FROM ITS LABELS, NOT ITS KEY (rega, row 43 case 3: this reported
|
||||
// ★ A DIALECT'S NAME COMES FROM ITS LABELS, NOT ITS KEY (a measured case: this reported
|
||||
// ", Afro-Asiatic__Arabic__Sudanese"). `selectedDialect.key` is the TREE NODE ID —
|
||||
// family__baseLang__dialect — and the watcher fifteen lines above says so in as many words;
|
||||
// I read that comment and then used the key as a display name anyway. Worse, selectedLanguage
|
||||
|
|
@ -2605,7 +2605,7 @@ function emitFilterState() {
|
|||
languageCodes: langCodes,
|
||||
}
|
||||
// ★ AN EMPTY REPORT AFTER A NON-EMPTY ONE IS AMBIGUOUS, SO IT IS NOT SENT IMMEDIATELY.
|
||||
// rega, row 43 case 3 at 52eeb0db: coalescing worked — one event per click — and in 3 of 4 runs
|
||||
// measured at 52eeb0db: coalescing worked — one event per click — and in 3 of 4 runs
|
||||
// that one event was EMPTY, so the grid stayed at 2,107. The cascade behind a dialect click
|
||||
// passes through a fully-cleared state: mapStore.selectLanguage() sets selectedDialect = null
|
||||
// BEFORE it sets the language, and the clear was the last write my watcher saw. The settle timer
|
||||
|
|
@ -2642,7 +2642,7 @@ function emitFilterState() {
|
|||
host.dispatchEvent(new CustomEvent('map-filtered', { detail, bubbles: true, composed: true }))
|
||||
}
|
||||
|
||||
// ★ ONE EVENT PER SETTLED SELECTION (rega, row 43 case 3: a dialect click narrowed the grid in
|
||||
// ★ ONE EVENT PER SETTLED SELECTION (a measured case: a dialect click narrowed the grid in
|
||||
// only ONE of four runs, with a correct languageCode in all four).
|
||||
//
|
||||
// THE CAUSE IS A BURST MEETING A GUARD. Clicking a dialect does not change one store value; it
|
||||
|
|
@ -2665,7 +2665,7 @@ const EMIT_SETTLE_MS = 60
|
|||
// of delay on a clear — about a fifth of a second, well under what a reader notices, and far below
|
||||
// the composite's own 1.2s push window so it cannot collide with that.
|
||||
// ★ A CEILING ON BELIEVING A CLEAR — NOT A GUESS AT HOW LONG THE CASCADE TAKES.
|
||||
// rega, row 43 case 3 at 835ec1ac: the retry budget catches the transient on MapLibre and NOT
|
||||
// measured at 835ec1ac: the retry budget catches the transient on MapLibre and NOT
|
||||
// reliably on Mapbox (and not at all in ar/mapbox), because 3 x 60ms is still a duration I picked
|
||||
// without being able to measure the thing it has to outlast. That is the THIRD time I have tuned a
|
||||
// number on one machine for this one defect, so the number's JOB changes rather than its value.
|
||||
|
|
@ -2682,7 +2682,7 @@ let _emptyWaitStartedAt = 0
|
|||
let _lastEmitWasFilled = false
|
||||
function emitFilterStateSettled() {
|
||||
// ★ THE ECHO GUARD MUST BE READ WHEN THE CHANGE HAPPENS, NOT WHEN THE TIMER FIRES.
|
||||
// rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event, on both
|
||||
// measured: a region row click emitted {region:"asia"} and then an EMPTY event, on both
|
||||
// engines. It passed only because the composite's 1.2s push window happened to swallow the
|
||||
// second one — a pass that depends on a swallow is not a pass.
|
||||
//
|
||||
|
|
@ -2809,7 +2809,7 @@ function applyFitSlugs(slugs) {
|
|||
: null
|
||||
|
||||
// ★ REDUCED MOTION DRIVES THE MAP DIRECTLY, and that is not belt-and-braces.
|
||||
// MEASURED by rega with the browser's prefers-reduced-motion: reduce set: the framing move still
|
||||
// MEASURED with the browser's prefers-reduced-motion: reduce set: the framing move still
|
||||
// animated for ~5s on MapLibre and ~6.7s on Mapbox — essentially the same as without it, where a
|
||||
// jump is ~0ms. So the preference was being honoured in THIS file and thrown away downstream:
|
||||
// useMapFly's flyToCoords(coords, zoom) takes NO options argument at all, so the `{ duration: 0 }`
|
||||
|
|
@ -2840,7 +2840,7 @@ function applyFitSlugs(slugs) {
|
|||
//
|
||||
// The checkbox force-set 'mst' "regardless of what's in the store", and the picker wrote to a
|
||||
// property mapStore never declared — so choosing Native drew nothing, and had it worked, one tick
|
||||
// of the checkbox would have snapped it back to MST anyway. MEASURED as inert by exp-shepherd; the
|
||||
// of the checkbox would have snapped it back to MST anyway. MEASURED as inert; the
|
||||
// second half is what makes fixing only the picker the wrong fix, because the control would have
|
||||
// worked until the reader touched the other one.
|
||||
//
|
||||
|
|
@ -2916,7 +2916,7 @@ async function onMapReady(normalizedPeopleGroups) {
|
|||
// added with a CONSTANT "circle-opacity": 1 (see addLanguageFamilyLayer), and applyPinOpacity is
|
||||
// otherwise first reached from a legend click — so without this line the expression shape below is
|
||||
// only ever the CLEARED state and the first selection is still the moment the variant is compiled,
|
||||
// which is the 3.2-3.5s freeze rega profiled. Found by reading the built bundle rather than the
|
||||
// which is the 3.2-3.5s freeze seen in profiling. Found by reading the built bundle rather than the
|
||||
// source: the addLayer constant is right there in the shipped JS.
|
||||
// matchExpr is left UNDEFINED on purpose — "re-apply what is current" — so a selection already
|
||||
// restored from a share-link hash is preserved instead of being cleared at boot.
|
||||
|
|
@ -3022,7 +3022,7 @@ const reannounceInfoPending = () => { if (!appReady.value && !mapError.value) an
|
|||
watch(mapError, (e) => { if (e) announceInfoPending(false) })
|
||||
// ★ THE PRAYER LINE'S TABLE IS FETCHED AFTER THE MAP MOUNTS, when the browser is idle, not when a print starts: the
|
||||
// print waits at most 3 s for it (openPoster), and on a slow phone network a fetch begun at the tap missed that window
|
||||
// (rega: the line dropped in 1 of 2 runs). loadPacketStrings caches the promise, so the print reads it at once; it
|
||||
// (the line dropped in 1 of 2 runs). loadPacketStrings caches the promise, so the print reads it at once; it
|
||||
// never rejects, and a failed fetch is retried by the print. 48 KB gzipped, once per page.
|
||||
onMounted(() => {
|
||||
const prefetch = () => import('@map/packet/packetStrings.js').then(({ loadPacketStrings }) => loadPacketStrings(resolveAssetsBase(profileConfig?.value))).catch(() => {})
|
||||
|
|
@ -3135,7 +3135,7 @@ onMounted(async () => {
|
|||
window.addEventListener('resize', _resyncScrollZoom)
|
||||
})
|
||||
|
||||
// ★ THE MAP IS TORN DOWN NO MATTER WHAT ELSE FAILS. rega's heap snapshots (leak-722ac7789) found
|
||||
// ★ THE MAP IS TORN DOWN NO MATTER WHAT ELSE FAILS. heap snapshots (leak-722ac7789) found
|
||||
// every unmounted research map still alive, held by window.__doxaLogoOwner -> Map -> div.rm-map-canvas
|
||||
// -> ShadowRoot -> app, and that global is released only when the map's 'remove' event fires. So a
|
||||
// single throw anywhere in this hook stranded the entire application in memory, one copy per mount.
|
||||
|
|
@ -3158,7 +3158,7 @@ onBeforeUnmount(() => {
|
|||
step(() => selectedPin.cleanup?.())
|
||||
step(() => mapStore.unregisterMap?.(mapId))
|
||||
// Every verify hook this component plants is an arrow closing over it, so each one left behind
|
||||
// keeps the component, its shadow root and the whole app alive (rega, leak-722ac7789). They are
|
||||
// keeps the component, its shadow root and the whole app alive (leak-722ac7789). They are
|
||||
// released as a set: they are installed as a set, and releasing three of four leaks just as much
|
||||
// as releasing none. Each is checked before deleting so a later mount's hook is never removed.
|
||||
// Written out one per line, not looped over an array of names, SO THE CHECK CAN READ IT.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* different. A control that does nothing is worse than a control that is absent: it tells the
|
||||
* reader the map cannot do what it can.
|
||||
*
|
||||
* THE DEFECT, MEASURED BY exp-shepherd AND CONFIRMED HERE. The side menu offers four clustering
|
||||
* THE DEFECT, MEASURED AND CONFIRMED HERE. The side menu offers four clustering
|
||||
* options; choosing "Native" changed nothing on the map. Two separate reasons, and only fixing both
|
||||
* makes the control real:
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* check-language-code-link.mjs — the map's language selection must reach the grid, or narrow nothing.
|
||||
*
|
||||
* THE BUG (rega, 2026-09-26, main 49992a5d, MapLibre, BOTH locales). Languages > Language >
|
||||
* THE BUG (measured on main 49992a5d, MapLibre, BOTH locales). Languages > Language >
|
||||
* "Arabic": the map emitted `map-filtered` with {language:"Arabic", languageCode:""}, the composite
|
||||
* handed the grid an empty code, and the grid went 2,107 -> 0. An empty grid reads as "no results",
|
||||
* which is the worst shape a wiring bug can take — the reader believes the data.
|
||||
|
|
@ -48,11 +48,11 @@
|
|||
* E. no tier may produce a value that selects zero rows. That is the user-visible invariant and
|
||||
* it is asserted directly, not inferred.
|
||||
* F. a DIALECT reports a NAME and not a tree node id, and the emitter sends ONE event per
|
||||
* settled selection rather than a burst. Both from rega's row 43 case 3.
|
||||
* settled selection rather than a burst. Both from a measured case.
|
||||
* G. that ONE event is not the cascade's transient EMPTY state. Same case, second measurement:
|
||||
* the coalescing worked and the single event was EMPTY in 3 of 4 runs.
|
||||
*
|
||||
* NO NETWORK, NO BROWSER (Playwright is rega's alone). The fixture is committed and was taken from
|
||||
* NO NETWORK, NO BROWSER (Playwright is the browser-QA lane's alone). The fixture is committed and was taken from
|
||||
* the live source; refresh it with --refresh-fixture when the source's language set changes.
|
||||
*
|
||||
* NEGATIVE-TESTED 2026-09-26, each FAILS:
|
||||
|
|
@ -221,7 +221,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── E. the dialect tier: a name, and one event ───────────────────────────────────────────────
|
||||
// rega, row 43 case 3, on BOTH engines in BOTH locales: clicking "Arabic, Sudanese" emitted the
|
||||
// Measured on BOTH engines in BOTH locales: clicking "Arabic, Sudanese" emitted the
|
||||
// right languageCode and narrowed the grid in only ONE of four runs, while the `language` slot read
|
||||
// ", Afro-Asiatic__Arabic__Sudanese".
|
||||
// THE NAME: selectedDialect.key is the TREE NODE ID (family__baseLang__dialect) and
|
||||
|
|
@ -258,7 +258,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── G. an empty report after a filled one is re-taken, and the re-take is BOUNDED ────────────
|
||||
// rega, row 43 case 3 at 52eeb0db: coalescing landed — one event per click — and in 3 of 4 runs
|
||||
// measured at 52eeb0db: coalescing landed — one event per click — and in 3 of 4 runs
|
||||
// that one event was EMPTY, so the grid stayed at 2,107. The cascade behind a dialect click passes
|
||||
// through a fully-cleared store state (mapStore.selectLanguage sets selectedDialect = null BEFORE
|
||||
// setting the language), and the settle timer fired inside it.
|
||||
|
|
@ -272,7 +272,7 @@ if (matchesLanguage) {
|
|||
if (!/_lastEmitWasFilled/.test(src) || !/const isEmpty/.test(src)) {
|
||||
fails.push('G research-map.vue no longer guards an all-empty report. A dialect click passes through a fully-cleared store state, so the settled read can be taken mid-cascade and ship {language:"",languageCode:""} — which the composite correctly reads as "the reader cleared everything".')
|
||||
} else if (!/EMPTY_SETTLE_CEILING_MS/.test(src)) {
|
||||
// ★ THE BOUND'S SHAPE CHANGED AND THE INVARIANT DID NOT. It used to be a retry COUNT; rega
|
||||
// ★ THE BOUND'S SHAPE CHANGED AND THE INVARIANT DID NOT. It used to be a retry COUNT; it was
|
||||
// measured that 3 x 60ms catches the cascade on MapLibre and not on Mapbox, so the count became
|
||||
// a TIME CEILING — the difference being what the number is FOR. A count is a guess at how long
|
||||
// the cascade takes, which cannot be known from here; a ceiling is how long before a clear is
|
||||
|
|
@ -285,7 +285,7 @@ if (matchesLanguage) {
|
|||
}
|
||||
|
||||
// ── H. the echo guard survives the coalescing ────────────────────────────────────────────────
|
||||
// rega, row 50: a region click emitted the selection and then an EMPTY event, passing only because
|
||||
// Measured: a region click emitted the selection and then an EMPTY event, passing only because
|
||||
// the composite's 1.2s push window swallowed the second. emitFilterState has always opened with
|
||||
// `if (applyingExternal) return` — a change pushed DOWN by the host is not a change to report back
|
||||
// up — and that worked while the emit ran synchronously inside the watcher. Debouncing moved the
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* ★ A READER OF THIS MAP IS PHOTOSENSITIVE. This is not a polish item: a camera that sweeps the globe every
|
||||
* time a list is filtered is the strobe class this repo has already paid for once.
|
||||
*
|
||||
* THE DEFECT, measured by rega in a real browser with the preference set: the framing move still
|
||||
* THE DEFECT, measured in a real browser with the preference set: the framing move still
|
||||
* animated for ~5.0s on MapLibre and ~6.7s on Mapbox — indistinguishable from no preference at all,
|
||||
* where a jump is ~0ms. The preference WAS being read correctly in research-map.vue and then thrown
|
||||
* away one layer down: useMapFly's `flyToCoords(coords, zoom)` takes NO options argument, so the
|
||||
|
|
@ -13,13 +13,13 @@
|
|||
* options)` does accept one — two functions in one file, different signatures, and nothing at the
|
||||
* call site to show it.
|
||||
*
|
||||
* ★ WHY THIS RUNS WITHOUT A BROWSER, WHICH IS THE POINT. rega found it by watching movestart →
|
||||
* moveend for six seconds; only rega can do that, and only occasionally. What can be checked any
|
||||
* ★ WHY THIS RUNS WITHOUT A BROWSER, WHICH IS THE POINT. it was found by watching movestart →
|
||||
* moveend for six seconds; only a real browser can do that, and only occasionally. What can be checked any
|
||||
* time, in milliseconds, is the DECISION: given the preference, which camera call does the code
|
||||
* make? So this lifts the real camera block out of the SFC, runs it against a mocked matchMedia and
|
||||
* a recording map, and asserts the call that comes out.
|
||||
*
|
||||
* It cannot prove the engine honours duration 0 — that is rega's half. It proves we ASK for a jump,
|
||||
* It cannot prove the engine honours duration 0 — that is the browser half. It proves we ASK for a jump,
|
||||
* which is the half that was wrong.
|
||||
*
|
||||
* ASSERTIONS:
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ const filtersBackdrop = computed(() => {
|
|||
})
|
||||
const prayBaseUrl = computed(() => props.prayBaseUrl || cfg.value.prayBaseUrl || 'https://pray.doxa.life')
|
||||
// ★ THE URL IS A SOURCE OF TRUTH FOR THE DATA TOO, not just for the chrome.
|
||||
// MEASURED by rega at ?lang=ar and ?lang=es: this grid's CHROME translated and its DATA stayed
|
||||
// MEASURED at ?lang=ar and ?lang=es: this grid's CHROME translated and its DATA stayed
|
||||
// English — country names, affinity names, religion labels — because the `&lang=` on the fetch was
|
||||
// built from props and profile-config ONLY. A reader who asked for Arabic got Arabic buttons around
|
||||
// English content, which reads as a half-finished translation rather than the wiring bug it is.
|
||||
|
|
@ -420,7 +420,7 @@ const t = computed(() => {
|
|||
const EAGER_IMAGES = 5
|
||||
const truthy = (v) => !(v === false || v === 'false' || v === '0' || v === 0)
|
||||
|
||||
// ★ THE PRESET'S KEYS WERE NEVER REACHING THESE (rega RQ20). The "driven" preset asks for
|
||||
// ★ THE PRESET'S KEYS WERE NEVER REACHING THESE (measured, RQ20). The "driven" preset asks for
|
||||
// show-search:false and show-filters:false and the page rendered both anyway, with 0 console
|
||||
// errors — because nothing was failing. ProfileLoader PROVIDES profile-config; it does not bind
|
||||
// its keys as props. So props.showSearch stayed at its default of true and the preset was inert.
|
||||
|
|
@ -1153,7 +1153,7 @@ onMounted(() => {
|
|||
<div id="results" class="grid uupgs-list" :style="gridStyle">
|
||||
<template v-for="(uupg, ui) in displayedUUPGs" :key="uupg.slug">
|
||||
<div class="card uupg__card">
|
||||
<!-- ★ THE PHOTOS WERE COMPETING WITH THE MAP FOR THE FIRST THREE SECONDS. rega measured a
|
||||
<!-- ★ THE PHOTOS WERE COMPETING WITH THE MAP FOR THE FIRST THREE SECONDS. Measurement showed a
|
||||
cold load of Simple Research pulling 24 people-group photos, 5.3 MB, DURING BOOT —
|
||||
alongside the map engine and the API data, on the same connection. Nothing was wrong
|
||||
with any one image; there was simply no instruction about WHEN to fetch them.
|
||||
|
|
|
|||
|
|
@ -713,7 +713,7 @@ useShadowStyles(
|
|||
{{ tr('finish', { n: drawPoints.length }) }}
|
||||
</button>
|
||||
<!-- PHONE: these three collapse behind "More" (they ran past x=390 in a sideways row with no
|
||||
scroll cue, rega ka3). Desktop: the toggle is hidden and they sit inline as before. -->
|
||||
scroll cue, measured ka3). Desktop: the toggle is hidden and they sit inline as before. -->
|
||||
<button type="button" class="pwp-btn pwp-more-btn" :aria-expanded="moreOpen ? 'true' : 'false'" @click="moreOpen = !moreOpen">⋯ {{ tr('more') }}</button>
|
||||
<div class="pwp-listops" :class="{ 'is-open': moreOpen }" @click="moreOpen = false">
|
||||
<button type="button" class="pwp-btn is-danger" :disabled="selectedCount === 0"
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ function onMapFiltered(ev) {
|
|||
})
|
||||
}
|
||||
|
||||
// ★ AN AXIS THIS GRID CANNOT EXPRESS MUST NOT NARROW IT TO NOTHING (rega, 2026-09-26: Languages >
|
||||
// ★ AN AXIS THIS GRID CANNOT EXPRESS MUST NOT NARROW IT TO NOTHING (measured: Languages >
|
||||
// Language > "Arabic" took the grid 2,107 -> 0 in BOTH locales, reading as "no results").
|
||||
//
|
||||
// This line used to be `linked.language.value || linked.language.label`, and the fallback was the
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ for (const p of catalogues) {
|
|||
const files = listFiles((p) => /\.(vue|js|mjs)$/.test(p) && !p.includes('/scripts/') && !p.includes('/locales/'))
|
||||
|
||||
// ── ★ A TRANSLATOR CAN ARRIVE FROM A COMPOSABLE, NOT ONLY FROM A LOCAL WRAPPER ───────────────
|
||||
// go-int planted a poison key behind `const T = useGiT()` and this gate PASSED. Three failures of
|
||||
// a poison key was planted behind `const T = useGiT()` and this gate PASSED. Three failures of
|
||||
// the same shape stacked up: (1) the file never writes the word tStatic, so the precondition below
|
||||
// skipped it outright; (2) `useGiT` is not `tStatic`, so aliasesIn() found no alias at all; and
|
||||
// (3) the alias is PREFIXED — useOptionalT carries a namespace — while its call sites pass BARE
|
||||
|
|
@ -267,7 +267,7 @@ function aliasesIn(src) {
|
|||
}
|
||||
|
||||
// ── ★ SELF-TEST, BECAUSE THIS GATE ALREADY PASSED A PLANTED POISON KEY ────────────────────────
|
||||
// go-int planted `T('poison_key')` behind `const T = useGiT()` and this gate reported the tree
|
||||
// a poison key `T('poison_key')` was planted behind `const T = useGiT()` and this gate reported the tree
|
||||
// clean. That is the failure a check cannot be forgiven for: a red gate gets fixed, a green one that
|
||||
// should be red gets BELIEVED, and this one guards goal (d). Measured before and after, with the
|
||||
// same poison on disk: the version on main PASSES it, this version FAILS it as
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ try {
|
|||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
||||
// ★ LISTEN BEFORE THE PAGE RUNS. The grid reports its total on its FIRST filter pass, which can happen
|
||||
// before an evaluate() after goto attaches a listener; the gate then read "the grid never reported a
|
||||
// total" while the grid had reported one (rega, ded4b3c1). Every total is recorded from the start.
|
||||
// total" while the grid had reported one (measured, ded4b3c1). Every total is recorded from the start.
|
||||
await page.addInitScript(() => {
|
||||
window.__lvTotals = []
|
||||
document.addEventListener('groups-filtered', (e) => { window.__lvTotals.push(e.detail?.total ?? null) }, true)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*
|
||||
* So this reads what the compiler reads and asks the one question those gates cannot: is every name
|
||||
* this file USES actually BOUND somewhere — imported, declared, a parameter, or a known global?
|
||||
* No browser, no mount, milliseconds per file. It is the cheap half of the pair; exp-shepherd's
|
||||
* No browser, no mount, milliseconds per file. It is the cheap half of the pair; the browser lane's
|
||||
* check:composites-mount is the expensive half that proves the thing actually runs.
|
||||
*
|
||||
* SCOPE: every .vue under plugin-designs/ and library/ — fleet-wide, because the mistake was a
|
||||
|
|
|
|||
|
|
@ -534,7 +534,7 @@ function connect() {
|
|||
clearTimeout(retry)
|
||||
try { es?.close() } catch {}
|
||||
// No EventSource (a headless mount, an old WebView): show the engine-not-running state instead of throwing
|
||||
// in the mounted hook, which failed check:profiles-mount for every lane (go-int, END-DXS-ESRC).
|
||||
// in the mounted hook, which failed check:profiles-mount for every lane.
|
||||
if (typeof window === 'undefined' || typeof window.EventSource !== 'function') {
|
||||
engineError.value = 'The desktop engine is not running.'
|
||||
return
|
||||
|
|
@ -665,7 +665,7 @@ function releaseMic() {
|
|||
media = null
|
||||
actx = null
|
||||
}
|
||||
// Stop: the mic goes off and the widget is idle at once (rega: it read "Stop" for 12 s+ while a loaded
|
||||
// Stop: the mic goes off and the widget is idle at once (it read "Stop" for 12 s+ while a loaded
|
||||
// server caught up). The socket stays open so the last words still land in the box; the engine closes the
|
||||
// session with "recording false". Record again meanwhile simply lets go of that tail.
|
||||
function stopBrowser() {
|
||||
|
|
@ -790,7 +790,7 @@ function onKey(ev) {
|
|||
if (ev.ctrlKey && ev.shiftKey && ev.code === 'Space') { ev.preventDefault(); toggle() }
|
||||
}
|
||||
|
||||
// The status also by plain request: a proxy that buffers the event stream (rega's harness did) must not
|
||||
// The status also by plain request: a proxy that buffers the event stream (as a test harness did) must not
|
||||
// leave the page guessing whether this server is meant to have an engine.
|
||||
async function fetchStatus() {
|
||||
try { const r = await fetch(apiBase.value + '/status'); if (r.ok) applyStatus(await r.json()) } catch {}
|
||||
|
|
@ -1160,7 +1160,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
|
|||
.compact .notice, .compact .hint { order: 1; font-size: 12.5px; padding: 8px 10px; }
|
||||
/* ★ AND THE NOTICE YIELDS. Ordering it below the draft fixes the reading order but NOT the height:
|
||||
.notice is flex:0 1 auto and .compact .compose is flex:1 1 0, so the draft still gives up whatever
|
||||
the notice's content asks for. rega measured the result at 375x450 — a 20px draft whose wheel moves
|
||||
the notice's content asks for. Measurement showed the result at 375x450 — a 20px draft whose wheel moves
|
||||
nothing. The engine-offline notice is the long one (apiBase, Go 1.22, the setup command), ~132px at
|
||||
375 and ~179px at 390, and it is REFERENCE text: a reader needs it once, not a third of a short
|
||||
panel forever. Capping it and letting it scroll gives the draft 58px at 375x450 and 124px at
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const fc = (features) => ({ type: 'FeatureCollection', features })
|
|||
// Hover label: after a language is picked, its believers (green) and the unreached they reach (red)
|
||||
// were anonymous until clicked, and a green dot could not be clicked at all. Same label as Mobilize.
|
||||
const hover = ref(null) // { x, y, text }
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (rega saw "Ko…"
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (a run saw "Ko…"
|
||||
// at the right edge in ar); max-width keeps it inside half the map on a phone. A tip is stale as soon as
|
||||
// the map moves (a tap, then the fly to the pick), so moving clears it.
|
||||
const hoverStyle = computed(() => {
|
||||
|
|
@ -110,7 +110,7 @@ function repaint() {
|
|||
const m = map.value
|
||||
if (!m) return
|
||||
// isStyleLoaded() is also false while TILES load (MapLibre), so a data load that lands then used to
|
||||
// be dropped for good: the start view showed no dots at all (rega, upgn6). Try again once idle.
|
||||
// be dropped for good: the start view showed no dots at all (measured, upgn6). Try again once idle.
|
||||
if (!m.isStyleLoaded?.()) { if (!repaintPending) { repaintPending = true; m.once('idle', () => { repaintPending = false; repaint() }) } return }
|
||||
const b = active.value
|
||||
setSrc(m, SRC_B, bridgeFeatures())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// useNeighborT — My UPG Neighbors' UI text: library/i18n/locales/<lc>/neighbor.json (11 locales).
|
||||
// Chrome only, by decision (dx-shepherd, 2026-09-26): research terms, provenance lines and proper
|
||||
// Chrome only, by decision: research terms, provenance lines and proper
|
||||
// nouns (SEND, doxa-2000, Bounded set, JP, PPLR…) stay in English as named sources.
|
||||
// i18n is OPTIONAL: useI18n() throws on a host that never installed it, so the catalogue is the
|
||||
// fallback, never a blank or a raw key. Call it inside setup().
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const believersNear = computed(() => neighbors.value.reduce((a, n) => a + n.g.be
|
|||
const peopleReachable = computed(() => neighbors.value.reduce((a, n) => a + n.g.pop, 0))
|
||||
|
||||
// A pick raises the phone sheet first, so the frame below is computed for the sheet that will show
|
||||
// the pick's detail (rega, 390: a pick with the list lowered showed only a tooltip).
|
||||
// the pick's detail (measured at 390: a pick with the list lowered showed only a tooltip).
|
||||
function select(g) { if (g) { nbState.sheetDown = false; nbState.pgid = g.pgid; query.value = '' } }
|
||||
|
||||
// The neighbor in focus: a dot click picks its card, a card click flies to its dot. Both ways, so the
|
||||
|
|
@ -95,7 +95,7 @@ const activeId = ref(null)
|
|||
// Hover label: 7,000 red dots are anonymous until clicked, so name the one under the pointer.
|
||||
const hover = ref(null) // { x, y, text }
|
||||
function hoverAt(ev, text) { hover.value = text ? { x: ev.point.x, y: ev.point.y, text } : null }
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (rega saw "Ko…"
|
||||
// The tip opens toward the side with more room, so near an edge it is never cut off (a run saw "Ko…"
|
||||
// at the right edge in ar); max-width keeps it inside half the map on a phone. A tip is stale as soon as
|
||||
// the map moves (a tap, then the fly to the pick), so moving clears it.
|
||||
const hoverStyle = computed(() => {
|
||||
|
|
@ -332,7 +332,7 @@ function repaint() {
|
|||
const m = map.value
|
||||
if (!m) return
|
||||
// isStyleLoaded() is also false while TILES load (MapLibre), so a data load that lands then used to
|
||||
// be dropped for good: the start view showed no dots at all (rega, upgn6). Try again once idle.
|
||||
// be dropped for good: the start view showed no dots at all (measured, upgn6). Try again once idle.
|
||||
if (!m.isStyleLoaded?.()) { if (!repaintPending) { repaintPending = true; m.once('idle', () => { repaintPending = false; repaint() }) } return }
|
||||
ensureLayers(m)
|
||||
}
|
||||
|
|
@ -357,7 +357,7 @@ function framePadding() {
|
|||
const rtl = !!el && getComputedStyle(el).direction === 'rtl'
|
||||
return w > 760 ? { top: 60, bottom: 60, left: rtl ? 60 : 400, right: rtl ? 400 : 60 } : { top: 40, bottom: (nbState.sheetDown ? 56 : Math.round(h * 0.55)) + 20, left: 30, right: 30 }
|
||||
}
|
||||
// PHONE: the list covered 54% of the map with no way to put it down (rega, 390). A handle on the sheet
|
||||
// PHONE: the list covered 54% of the map with no way to put it down (measured at 390). A handle on the sheet
|
||||
// lowers it to a bar and raises it again; the choice is shared with Campaigns.
|
||||
function toggleSheet() { nbState.sheetDown = !nbState.sheetDown; nextTick(() => frame()) }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* MapLibre throws "Style is not done loading" from setProjection before that, and setProjectionOn
|
||||
* swallows the throw, so the lenses that set 'mercator' right after initializeMap() never got it on
|
||||
* MapLibre (rega, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
|
||||
* MapLibre (measured, C2: lang-cluster, send-map, the bloom cockpit's MapCell). Mapbox accepts it early,
|
||||
* which is why this only showed on one engine.
|
||||
*/
|
||||
export function whenStyleReady(map, fn) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue