1// The one styling of a grid cell, as pure tokens.2//3// What its boundary weighs, what fills it, what its corner label says. The4// chart adapts these into ol/style and the globe into materials and sprites;5// neither holds a colour or a width of its own, so the two projections read as6// one instrument (issue #5 §5.4: renderers adapt tokens; no renderer owns a7// cell rule).8//9// Two questions come from the renderer's side rather than the plan's:10// whether the subdivision is showing (session state), and whether the label11// fits where it would be drawn (a measurement only a renderer can make). A12// child cell too small for its label draws **nothing at all** — the13// subdivision appears at the size it can be read, on both panes at the same14// visual moment — which is why null is a real answer here.1516import type { CellSystem } from "./contract.ts";17import type { Ground } from "./ground.ts";18import type { PlanCell } from "./plan.ts";19import { gridTheme, paletteColor } from "./tokens.ts";2021/** The two things only the renderer knows. */22export interface VisualOptions {23 /** Whether the held cell's subdivision is showing. */24 readonly subgridVisible: boolean;25 /** Whether the label fits where this cell would carry it. */26 readonly labelled: boolean;27}2829export interface LineTokens {30 readonly color: string;31 readonly opacity: number;32 readonly widthPx: number;33}3435export interface FillTokens {36 readonly color: string;37 readonly opacity: number;38}3940export interface LabelTokens {41 /** The faint cut: everything before the principal character. */42 readonly prefix: string;43 /** The bright cut: the principal digit of the address. */44 readonly final: string;45 readonly color: string;46 readonly textAlpha: number;47 readonly chip: string;48 readonly sizePx: number;49}5051export interface CellVisual {52 readonly line: LineTokens;53 readonly fill: FillTokens | null;54 readonly label: LabelTokens | null;55 /** The held cell with its subdivision showing: an outline and nothing else. */56 readonly bare: boolean;57}5859/**60 * The style tokens for one plan cell, or null when the cell draws nothing.61 *62 * Pure: the same cell, options and ground always answer the same tokens, and63 * no token is a renderer's unit but the pixel widths, which both panes agree64 * on because they read them from here.65 */66export function gridCellVisual(67 ground: Ground,68 system: CellSystem,69 cell: PlanCell,70 { subgridVisible, labelled }: VisualOptions,71): CellVisual | null {72 const { role, contextDistance } = cell;73 if (role === "child" && (!labelled || !subgridVisible)) return null;74 const on = system.on(ground);75 const color = paletteColor(on.colorKey(cell.hash));76 const chosen = role === "leaf" || role === "scope";77 const bare = role === "scope" && subgridVisible;7879 const line: LineTokens = {80 color: chosen ? gridTheme.lineWhite : color,81 opacity: role === "neighbor"82 ? gridTheme.neighborLineAlpha83 : role === "child" ? gridTheme.childLineAlpha : 1,84 widthPx: bare ? gridTheme.widths.scopeBare : gridTheme.widths[role],85 };8687 let fill: FillTokens | null = null;88 if (role === "neighbor") {89 fill = {90 color: gridTheme.dimColor,91 opacity: Math.min(92 gridTheme.dimCap,93 gridTheme.dimBase + contextDistance * gridTheme.dimStep,94 ),95 };96 } else if (role === "leaf") {97 fill = { color, opacity: gridTheme.leafFillAlpha };98 } else if (role === "child") {99 fill = { color, opacity: gridTheme.childFillAlpha };100 }101102 let label: LabelTokens | null = null;103 if (labelled && !bare) {104 const { context, principal } = on.label(cell.hash);105 label = {106 prefix: context,107 final: principal,108 color: chosen ? gridTheme.lineWhite : color,109 textAlpha: role === "neighbor" ? gridTheme.neighborTextAlpha : 1,110 chip: role === "neighbor" ? gridTheme.neighborChip : gridTheme.chip,111 sizePx: role === "neighbor" ? gridTheme.neighborLabelSizePx : gridTheme.labelSizePx,112 };113 }114 return { line, fill, label, bare };115}