All files / src/ui laneCarry.ts

100% Statements 33/33
100% Branches 23/23
100% Functions 6/6
100% Lines 33/33

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                                                                                                    2x 2x     2x 546x     546x 546x 223x 223x 546x     2x 7x 7x 7x     2x 29x 29x 29x     2x 4x 4x           2x 12x 15x 13x 15x 15x 4x 4x     2x 10x 10x 10x 10x  
/**
 * A lane carried by its grip past the map it is drawn on (§16.9).
 *
 * The grip drag is a **pointer** drag, captured on the lane canvas it began on
 * (`LaneGestures.watchLane`): every event goes to that canvas whatever is under
 * it, which is what stops the map below reading the drag as a pan — and what
 * stops anything else on the page hearing about it at all. A `dragover` never
 * fires. So the places a lane can be let go on say so here instead: each
 * *surface* — a map's edge rectangles, the deck's `+` edges and its cells —
 * answers what is under a point on the page, and the carrying panel asks, move
 * by move, and hands the lane to whatever answered when the hand lets go.
 *
 * What a landing does is its own business; what every one of them does is
 * **move** the lane: it is drawn where it landed and taken off the edge it came
 * from — off that edge only, so a track on two sides of a map keeps the side
 * nobody touched. A lane dragged to another cell is a reader saying *there, not
 * here*, and the Tracks card is where a track is put somewhere *as well*.
 */
 
import type { MapId } from '../annotations/placement'
import type { LaneEdge } from '../annotations/types'
 
/**
 * The lane in the hand: its tracks — an overlay group moves as one — the map
 * they are on, and **which edge** of it, since a track drawn along two sides of
 * one map is carried off the one the hand took hold of and stays on the other.
 * A tracks cell's lanes are on its one edge, the bottom.
 */
export interface LaneCarry {
  ids: readonly string[]
  from: MapId
  edge: LaneEdge
}
 
/** One place a carried lane can land. */
export interface LaneLanding {
  /** Lit while the carried lane would land here if let go. */
  light(on: boolean): void
  /** Take the lane: draw it here, and off the map it came from. */
  take(carry: LaneCarry): void
}
 
/** Something on the page with places a lane can land on. */
export interface LaneSurface {
  /** The landing under a point in viewport coordinates, if any. */
  landingAt(x: number, y: number, carry: LaneCarry): LaneLanding | null
  /** A lane was taken up, or put down: show the places, or stop. */
  carrying?(carry: LaneCarry | null): void
}
 
const surfaces = new Set<LaneSurface>()
let held: LaneCarry | null = null
 
/** Put a surface's landings on offer; the returned function takes them off. */
export function offerLanding(surface: LaneSurface): () => void {
  surfaces.add(surface)
  // A surface built while a lane is in the air — a cell the drop is about to
  // open, say — shows its places like every other.
  if (held !== null) surface.carrying?.(held)
  return () => {
    surfaces.delete(surface)
  }
}
 
/** A lane was taken up by its grip: every surface shows where it can go. */
export function startCarry(carry: LaneCarry): void {
  held = carry
  for (const surface of surfaces) surface.carrying?.(carry)
}
 
/** The lane was put down, wherever that was. */
export function endCarry(): void {
  held = null
  for (const surface of surfaces) surface.carrying?.(null)
}
 
/** The lane in the hand, or `null`. */
export function carried(): LaneCarry | null {
  return held
}
 
/**
 * The landing under a point, from any surface but `except` — the panel the lane
 * came from answers its own edges itself.
 */
export function landingAt(x: number, y: number, carry: LaneCarry, except?: LaneSurface): LaneLanding | null {
  for (const surface of surfaces) {
    if (surface === except) continue
    const landing = surface.landingAt(x, y, carry)
    if (landing !== null) return landing
  }
  return null
}
 
/** Whether a point in viewport coordinates is inside an element's box. */
export function within(node: Element, x: number, y: number): boolean {
  const box = node.getBoundingClientRect()
  if (box.width <= 0 || box.height <= 0) return false
  return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom
}