/** * The Steps view's model — what happens from an anchor, as typed steps laid * out so that a step sits above the steps it sets in motion. * * Everything geometric is the Screens view's (`screens-model.ts`): the Map's * layout with directional ports, a curve per edge on a track of its own, the * pills that label a selected step's links at the far end of each line, and * the nearest-line pointer. What is this file's own is small: the row a step * sits on is its distance from the anchor, which the server already counted * (`WireStep.depth`), so the layering is a lookup rather than a search; the * words in a box come from the step's kind; and the side panel's two lists * are the links into and out of the selected step. */ import { whenWords } from './conditions'; import type { WireMapLink, WireMapModule, WireStep, WireStepDecision, WireStepLink, WireStepTrigger, WireStepsPayload } from './wire'; import { buildMapLayout, linkId, nodeWidth, strokeWidthFor, NODE_GAP, NODE_HEIGHT, PADDING, PORT_PITCH, type EdgeRoute, type MapEdgeLayout, type MapLayout, type MapNodeLayout, type PortRef, } from './map-model'; import { edgeLabel, samplePolyline, trackedCurves, SCREEN_LAYER_GAP, type Curve, type Picture, type Point, } from './screens-model'; export interface StepNodeInfo { id: string; step: WireStep; /** What the box prints on its first line. */ label: string; /** …and on its second. */ sub: string; } export interface StepEdgeInfo { id: string; from: string; to: string; /** Every link between the pair — one connector, several stories. */ links: WireStepLink[]; /** The connector's short label: the innermost condition, or how many links. */ label: string; /** Every link behind it was synthesized (a dynamic-dispatch bridge). */ synthesized: boolean; /** The kind the links agree on, or `calls` when they differ. */ kind: WireStepLink['kind']; /** * The one way of a decision this connector is — `yes`, `no`, a case's * value — when it and a sibling out of the same box are arms of one fork. * The condition itself is said once, under the box ({@link StepDecision}). */ arm?: string; } /** * A decision drawn where it is made: the condition said ONCE, under the box * that decides it, while each line out of that box says only which way it is. */ export interface StepDecision { id: string; /** The condition as a reader says it, asking: `await hasSeenWelcome(…)?`. */ label: string; x: number; y: number; width: number; } export interface StepsModel extends Picture { layout: MapLayout; nodes: Map; edges: Map; layerGap: number; curves: Map; polylines: Map; /** Steps per kind, for the panel's summary. */ counts: Record; /** * A screen's picture only: the regions its boxes are laid out by, for the * captions. Null when the steps carry no regions — an endpoint's or a * function's picture, and the order reading — and the rows are distance. */ regions: StepRegionZone[] | null; /** The first box of each region — where the anchor's at-rest line arrives. */ regionEntries: ReadonlySet | null; /** * The order reading only: its decisions, each drawn as a point of its own * where the arms diverge (`fork:N` in the layout). Null on the tree, whose * decisions are made INSIDE a box and drawn under it ({@link decisions}). */ forks: Map | null; /** * The tree reading's decisions: a condition said once under the box that * decides it, its arms labelled on the lines out. Empty when the picture * holds none. */ decisions: StepDecision[]; } /** * A decision on the order reading's canvas — a fork of the code with two or * more arms that lead somewhere. The condition is said ONCE, on the point, * and each line out answers it (`yes`, `no`, a case's value): two lines that * each carried the whole predicate, one of them negated, never said they were * the same choice. */ export interface StepForkInfo { id: string; /** The condition in positive words — a switch's subject; '' when the arms share none. */ on: string; form: 'if' | 'switch' | 'ternary' | 'try'; /** The point's words: the condition, asked — `user AND (await …)?`. */ label: string; } /** One region of a screen's picture: its caption, and the space its boxes hold. */ export interface StepRegionZone { id: string; label: string; x: number; y: number; width: number; height: number; /** The region's first box, in the walk's order. */ entry: string; } /** Points a curve is sampled at for hit-testing (as the Screens view's). */ const HIT_SAMPLES = 24; /* ---------------------------------------------------------------- words -- */ /** What the index is a picture of; the server decides it from the routes (`WireStepsPayload.project`). */ export type ProjectKind = WireStepsPayload['project']; /** * A short word for a step's kind, as the panel and the legend say it — in the * project's own vocabulary. The same box is a screen in an app, a page in a * web app and an endpoint in an API; a route that leads with an HTTP verb is * an endpoint wherever it is. One place decides, so the legend, the panel * and the tooltip never disagree. */ export function kindWord(kind: WireStep['kind'], project: ProjectKind = 'app', step?: WireStep): string { return kindWords(kind, project, step)[0]; } /** The singular and the plural, for counts: `1 endpoint`, `3 outside the index`. */ export function kindWords(kind: WireStep['kind'], project: ProjectKind = 'app', step?: WireStep): [string, string] { switch (kind) { case 'screen': if (step?.screen?.endpoint) return ['endpoint', 'endpoints']; return project === 'api' ? ['endpoint', 'endpoints'] : project === 'web' ? ['page', 'pages'] : ['screen', 'screens']; case 'trigger': return ['handler', 'handlers']; case 'bridge': // An endpoint reached across a tier is a call to the server wherever it is. if (step?.screen?.endpoint) return ['call to the server', 'calls to the server']; return project === 'app' ? ['native call', 'native calls'] : project === 'web' ? ['call to the server', 'calls to the server'] : ['call to another tier', 'calls to another tier']; case 'event': return project === 'app' ? ['native event', 'native events'] : project === 'web' ? ['arrives from the server', 'arrive from the server'] : ['arrives from a queue or bus', 'arrive from a queue or bus']; case 'store': return project === 'api' ? ['data call', 'data calls'] : ['store action', 'store actions']; case 'effect': return ['outside the index', 'outside the index']; default: return ['start', 'start']; } } /** `3 handlers`, `1 endpoint`, `11 outside the index`. */ export function countWords(n: number, kind: WireStep['kind'], project: ProjectKind = 'app'): string { const [one, many] = kindWords(kind, project); return `${n} ${n === 1 ? one : many}`; } /** * What fires something, in a few characters: `onPress ·