All files / src/ui calledSurfaces.ts

100% Statements 84/84
84.21% Branches 32/38
100% Functions 5/5
100% Lines 84/84

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132                                    2x 2x               2x 2x     2x   43x 43x 43x 16x 16x 16x 16x 7x 9x 16x 16x 27x 27x 27x 43x 14x 13x 11x 2x 43x 43x     2x 2x 2x 2x 2x 2x 2x             5x 5x 5x 10x 10x 10x 5x 5x 5x 5x 1x 1x 1x 5x 5x 5x                     2x 182x 182x 182x 182x 182x 14x 14x 15x 15x 15x 15x 15x   14x 14x 14x 14x 14x 16x 28x 28x 28x 14x 14x 14x 14x 1x 1x 1x 1x 1x 14x 14x 14x 14x  
/**
 * A called set's two dropdowns (§18.7): its **display** — a 1D track beside the
 * map, or an overlay on it — and, on the map, the **look** of a set of places.
 * A loaded set of pairs takes the display too (§8.1): arcs, or rings.
 *
 * One row and one choice, where there used to be two rows and a switch on each:
 * a run of insulation listed its boundaries twice and its domains twice, and
 * with both switches on — which is how every set arrived — drew each of them
 * twice. The display is the row's own (`analysis/surfaces.ts`'s `setDisplay`),
 * so a session remembers it with the row and a set run again keeps it.
 *
 * Offered in the drawer under the run that called the set and at the head of the
 * row's settings, which is where a reader looking at one surface asks for the
 * other. Not where there is no map: in the 1D browser a set is a track whatever
 * it is on a map, and a choice with one answer is not a control.
 */
 
import type { AnnotationStore } from '../annotations/store'
import { POINT_LOOKS, overlayShapeOf, type AnnotationEntry, type OverlayShape } from '../annotations/types'
import {
  displayOf,
  offersDisplay,
  offersLook,
  setDisplay,
  setLook,
  type CalledDisplay,
} from '../analysis/surfaces'
import { el } from './dom'
import { settingsField } from './laneSettings'
 
/** The display's two answers, in the words of the dropdown. */
const DISPLAYS: readonly CalledDisplay[] = ['lane', 'overlay']
 
function displayWords(entry: AnnotationEntry, display: CalledDisplay): { label: string; hint: string } {
  const pairs = entry.kind === 'pairs' || entry.payload?.kind === 'pairs'
  if (display === 'lane') {
    return {
      label: pairs ? 'arcs (1D track)' : '1D track',
      hint:
        entry.kind === 'pairs' || entry.payload?.kind === 'pairs'
          ? 'An arc joining the two ends of every pair, beside the map and in the 1D browser.'
          : 'Segments beside the map and in the 1D browser, each as wide as its call.',
    }
  }
  return {
    label: 'map overlay',
    hint:
      entry.kind === 'pairs' || entry.payload?.kind === 'pairs'
        ? 'A ring round each pair on the map.'
        : offersLook(entry)
          ? 'On the diagonal of the map it was called from, in the look beside it.'
          : 'The corner of each one’s square along the diagonal of the map it was called from.',
  }
}
 
/** A set of places' two looks on the map, in the words of the dropdown. */
const LOOK_WORDS: Readonly<Record<string, { label: string; hint: string }>> = {
  point: { label: 'point', hint: 'A dot on the diagonal at each place.' },
  border: {
    label: 'short diagonal marker',
    hint: 'A short rule across the diagonal at each place, which leaves the diagonal itself uncovered.',
  },
}
 
/**
 * The look of a set of places on the map: a point, or the short marker across
 * the diagonal. Rebuilt by {@link calledSetFields} whenever the display moves,
 * since it means nothing while the set is a track.
 */
function lookField(store: AnnotationStore, entry: AnnotationEntry): HTMLElement {
  const select = el('select', { class: 'select', attrs: { 'data-field': 'look' } })
  for (const look of POINT_LOOKS) {
    const words = LOOK_WORDS[look]
    select.appendChild(el('option', { value: look, text: words.label, title: words.hint }))
  }
  const current = overlayShapeOf(entry.style)
  select.value = current
  select.title = LOOK_WORDS[current]?.hint ?? ''
  select.addEventListener('change', () => {
    const picked: OverlayShape = POINT_LOOKS.find((look) => look === select.value) ?? current
    select.title = LOOK_WORDS[picked]?.hint ?? ''
    setLook(store, entry.id, picked)
  })
  return settingsField('look', select)
}
 
/**
 * The display, and the look beside it while the set is on the map — or `null`
 * for a row that is not a called set, and where there is no map to choose.
 *
 * @param options.map Whether there is a map here to draw an overlay on.
 * @param options.onDisplay Told after the display has moved, with its select:
 *   the Tracks card rebuilds the row's settings around it, since a track and an
 *   overlay have different controls.
 */
export function calledSetFields(
  store: AnnotationStore,
  entry: AnnotationEntry,
  options: { map: boolean; onDisplay?: (select: HTMLSelectElement) => void },
): HTMLElement | null {
  if (!offersDisplay(entry) || !options.map) return null
  const look = el('span', { class: 'ann-called-look' })
  const fillLook = (): void => {
    const live = store.byId(entry.id) ?? entry
    look.replaceChildren(
      ...(displayOf(live) === 'overlay' && offersLook(live) ? [lookField(store, live)] : []),
    )
  }
 
  const select = el('select', {
    class: 'select',
    attrs: { 'data-field': 'display', 'data-display': entry.id },
  })
  const title = (display: CalledDisplay): string => displayWords(store.byId(entry.id) ?? entry, display).hint
  for (const display of DISPLAYS) {
    const words = displayWords(entry, display)
    select.appendChild(el('option', { value: display, text: words.label, title: words.hint }))
  }
  const current = displayOf(entry)
  select.value = current
  select.title = title(current)
  select.addEventListener('change', () => {
    const picked: CalledDisplay = DISPLAYS.find((display) => display === select.value) ?? current
    setDisplay(store, entry.id, picked)
    select.title = title(picked)
    fillLook()
    options.onDisplay?.(select)
  })
  fillLook()
  return el('div', { class: 'ann-called' }, settingsField('display', select), look)
}