feat(steps): lay out screen pictures by region and render region captions
Adds region-based layout support for screens: steps now carry region information, and the server packs regions into dedicated bands with per-region captions. UI changes introduce RegionCaption and region-aware step rendering; StepsModel and related views (StepsView) consume region data, while the region-aware layout keeps anchor and region boundaries intact. Tests and docs updated to reflect region-driven organization and visualization of screen regions. This enables visualizing a screen’s picture as region-based columns rather than a single distance-driven row.
This commit is contained in:
@@ -269,7 +269,11 @@ export function buildOrderModel(payload: WireStepsPayload): StepsModel | null {
|
||||
minWeight: 0,
|
||||
sizing: (m) => {
|
||||
const info = nodes.get(m.id);
|
||||
return { label: info?.label ?? m.id, meta: info?.sub ?? '' };
|
||||
// Size for the ` …` a cut step wears and the anchor's ● mark, or the
|
||||
// CSS ellipsis eats the name's tail.
|
||||
const cut = info?.step.cut != null ? ' …' : '';
|
||||
const mark = info?.step.anchor ? '● ' : '';
|
||||
return { label: mark + (info?.label ?? m.id) + cut, meta: info?.sub ?? '' };
|
||||
},
|
||||
layering,
|
||||
order: (id) => nodes.get(id)?.step.order ?? Number.MAX_SAFE_INTEGER,
|
||||
@@ -281,7 +285,8 @@ export function buildOrderModel(payload: WireStepsPayload): StepsModel | null {
|
||||
const curves = trackedCurves(layout, SCREEN_LAYER_GAP);
|
||||
const polylines = new Map<string, Point[]>();
|
||||
for (const [id, curve] of curves) polylines.set(id, samplePolyline(curve, HIT_SAMPLES));
|
||||
return { layout, nodes, edges, layerGap: SCREEN_LAYER_GAP, curves, polylines, counts };
|
||||
// The order reading needs no regions: its rows already say when.
|
||||
return { layout, nodes, edges, layerGap: SCREEN_LAYER_GAP, curves, polylines, counts, regions: null, regionEntries: null };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+426
-26
@@ -13,7 +13,21 @@
|
||||
*/
|
||||
|
||||
import type { WireMapLink, WireMapModule, WireStep, WireStepLink, WireStepTrigger, WireStepsPayload } from './wire';
|
||||
import { buildMapLayout, linkId, PORT_PITCH, type MapLayout } from './map-model';
|
||||
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,
|
||||
@@ -56,6 +70,26 @@ export interface StepsModel extends Picture {
|
||||
polylines: Map<string, Point[]>;
|
||||
/** Steps per kind, for the panel's summary. */
|
||||
counts: Record<WireStep['kind'], number>;
|
||||
/**
|
||||
* 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<string> | null;
|
||||
}
|
||||
|
||||
/** 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). */
|
||||
@@ -243,34 +277,400 @@ export function buildStepsModel(payload: WireStepsPayload): StepsModel {
|
||||
});
|
||||
}
|
||||
|
||||
// Layer = distance from the anchor, counted by the server. Layer 0 is the
|
||||
// bottom, so the deepest row is 0 and the anchor is on top.
|
||||
const depthOf = new Map(payload.steps.map((s) => [s.id, s.depth]));
|
||||
const deepest = Math.max(0, ...payload.steps.map((s) => s.depth));
|
||||
const layering = (ids: string[]): Map<string, number> =>
|
||||
new Map(ids.map((id) => [id, deepest - (depthOf.get(id) ?? deepest)]));
|
||||
// A screen's picture is laid out by its REGIONS when the server named them
|
||||
// (`WireStep.region`): a screen is a set of handlers with no order between
|
||||
// them, so distance alone put ninety boxes on one enormous row. An
|
||||
// endpoint's or a function's picture keeps the rows: there, distance IS the
|
||||
// reading.
|
||||
const regioned = payload.steps.some((s) => s.region !== undefined);
|
||||
let layout: MapLayout;
|
||||
let zones: StepRegionZone[] | null = null;
|
||||
if (regioned) {
|
||||
const packed = packRegions(payload.steps, nodes, modules, links);
|
||||
layout = packed.layout;
|
||||
zones = packed.zones;
|
||||
} else {
|
||||
// Layer = distance from the anchor, counted by the server. Layer 0 is the
|
||||
// bottom, so the deepest row is 0 and the anchor is on top.
|
||||
const depthOf = new Map(payload.steps.map((s) => [s.id, s.depth]));
|
||||
const deepest = Math.max(0, ...payload.steps.map((s) => s.depth));
|
||||
const layering = (ids: string[]): Map<string, number> =>
|
||||
new Map(ids.map((id) => [id, deepest - (depthOf.get(id) ?? deepest)]));
|
||||
|
||||
const layout = buildMapLayout(
|
||||
{ modules, links },
|
||||
{
|
||||
includeTests: true,
|
||||
minWeight: 0,
|
||||
sizing: (m) => {
|
||||
const info = nodes.get(m.id);
|
||||
return { label: info?.label ?? m.id, meta: info?.sub ?? '' };
|
||||
},
|
||||
layering,
|
||||
// The server ordered each row the way the code reads; keep it.
|
||||
order: (id) => nodes.get(id)?.step.order ?? Number.MAX_SAFE_INTEGER,
|
||||
layerGap: SCREEN_LAYER_GAP,
|
||||
portPitch: PORT_PITCH,
|
||||
ports: 'directional',
|
||||
}
|
||||
);
|
||||
const curves = trackedCurves(layout, SCREEN_LAYER_GAP);
|
||||
layout = buildMapLayout(
|
||||
{ modules, links },
|
||||
{
|
||||
includeTests: true,
|
||||
minWeight: 0,
|
||||
sizing: (m) => {
|
||||
const info = nodes.get(m.id);
|
||||
// Size for the ` …` a cut step wears and the anchor's ● mark, as `packRegions` does.
|
||||
const cut = info?.step.cut != null ? ' …' : '';
|
||||
const mark = info?.step.anchor ? '● ' : '';
|
||||
return { label: mark + (info?.label ?? m.id) + cut, meta: info?.sub ?? '' };
|
||||
},
|
||||
layering,
|
||||
// The server ordered each row the way the code reads; keep it.
|
||||
order: (id) => nodes.get(id)?.step.order ?? Number.MAX_SAFE_INTEGER,
|
||||
layerGap: SCREEN_LAYER_GAP,
|
||||
portPitch: PORT_PITCH,
|
||||
ports: 'directional',
|
||||
}
|
||||
);
|
||||
}
|
||||
const layerGap = regioned ? REGION_GAP_Y : SCREEN_LAYER_GAP;
|
||||
const curves = trackedCurves(layout, layerGap);
|
||||
const polylines = new Map<string, Point[]>();
|
||||
for (const [id, curve] of curves) polylines.set(id, samplePolyline(curve, HIT_SAMPLES));
|
||||
return { layout, nodes, edges, layerGap: SCREEN_LAYER_GAP, curves, polylines, counts };
|
||||
return {
|
||||
layout,
|
||||
nodes,
|
||||
edges,
|
||||
layerGap,
|
||||
curves,
|
||||
polylines,
|
||||
counts,
|
||||
regions: zones,
|
||||
regionEntries: zones === null ? null : new Set(zones.map((z) => z.entry)),
|
||||
};
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- regions -- */
|
||||
|
||||
/**
|
||||
* The gap under a line of boxes within a region — tighter than the row gap of
|
||||
* an unregioned picture, whose gaps carry every line of a whole row's fan-out;
|
||||
* here a gap holds a few local hops, and a screen's picture is tall enough
|
||||
* already. The tracked curves take the same number, so a level arch stays
|
||||
* inside it.
|
||||
*/
|
||||
const REGION_GAP_Y = 72;
|
||||
/** The vertical rhythm of a regioned picture: one line of boxes and the gap under it. */
|
||||
const REGION_PITCH = NODE_HEIGHT + REGION_GAP_Y;
|
||||
/** A region's line of boxes wraps past this natural width. */
|
||||
const REGION_LINE_MAX = 720;
|
||||
/** Between two regions side by side. */
|
||||
const REGION_GUTTER = 72;
|
||||
/** Extra room between two rows of regions — the captions of the next row live in it. */
|
||||
const BAND_GAP = 84;
|
||||
/** Bands may run this wide: enough for the widest region, aiming at a readable aspect. */
|
||||
function bandBudget(area: number, widest: number): number {
|
||||
return Math.max(widest, Math.min(3400, Math.max(1440, Math.ceil(Math.sqrt(area * 2.4)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* The layout of a screen's picture: each region a small column of lines —
|
||||
* a box above what it sets in motion, a line wrapping when it grows past
|
||||
* {@link REGION_LINE_MAX} — and the regions tiled left to right, wrapping
|
||||
* into bands, in the order the walk met them: the screen's own source order.
|
||||
* The anchor sits alone on top. Everything downstream — the tracked curves,
|
||||
* the pills, the pointer — is the same machinery over the same shapes.
|
||||
*/
|
||||
function packRegions(
|
||||
steps: WireStep[],
|
||||
infos: Map<string, StepNodeInfo>,
|
||||
modules: WireMapModule[],
|
||||
links: WireMapLink[]
|
||||
): { layout: MapLayout; zones: StepRegionZone[] } {
|
||||
const anchor = steps.find((s) => s.anchor)!;
|
||||
const members = steps.filter((s) => !s.anchor);
|
||||
const moduleOf = new Map(modules.map((m) => [m.id, m]));
|
||||
|
||||
// A box is wide enough for its words and for its ports — the anchor touches
|
||||
// most of the picture, and its lines need somewhere to leave from.
|
||||
const degree = new Map<string, number>();
|
||||
for (const l of links) {
|
||||
degree.set(l.source, (degree.get(l.source) ?? 0) + 1);
|
||||
degree.set(l.target, (degree.get(l.target) ?? 0) + 1);
|
||||
}
|
||||
const widthOf = (id: string): number => {
|
||||
const info = infos.get(id);
|
||||
// A cut step wears ` …` after its name and the anchor its ● mark before
|
||||
// it; size for both, or the CSS ellipsis eats the name's tail instead
|
||||
// (`/scan-to-verif…` for `/scan-to-verify …`).
|
||||
const cut = info?.step.cut != null ? ' …' : '';
|
||||
const mark = info?.step.anchor ? '● ' : '';
|
||||
return Math.max(
|
||||
nodeWidth(mark + (info?.label ?? id) + cut, info?.sub ?? ''),
|
||||
((degree.get(id) ?? 0) + 1) * PORT_PITCH
|
||||
);
|
||||
};
|
||||
|
||||
// Regions in the order the walk met them — the screen's own source order.
|
||||
interface Region {
|
||||
id: string;
|
||||
label: string;
|
||||
members: WireStep[];
|
||||
}
|
||||
const regions = new Map<string, Region>();
|
||||
for (const s of members) {
|
||||
const id = s.region?.id ?? anchor.id;
|
||||
const region = regions.get(id) ?? { id, label: s.region?.label ?? anchor.label, members: [] };
|
||||
region.members.push(s);
|
||||
regions.set(id, region);
|
||||
}
|
||||
|
||||
// Within a region, a step goes under the steps that lead to it.
|
||||
const regionOf = new Map(members.map((s) => [s.id, s.region?.id ?? anchor.id]));
|
||||
const parentsOf = new Map<string, string[]>();
|
||||
for (const l of links) {
|
||||
if (l.source === anchor.id || l.target === anchor.id) continue;
|
||||
if (regionOf.get(l.source) !== regionOf.get(l.target)) continue;
|
||||
const list = parentsOf.get(l.target) ?? [];
|
||||
list.push(l.source);
|
||||
parentsOf.set(l.target, list);
|
||||
}
|
||||
|
||||
interface Packed {
|
||||
lines: string[][];
|
||||
width: number;
|
||||
}
|
||||
const packed = new Map<string, Packed>();
|
||||
for (const region of regions.values()) {
|
||||
// A step goes under the steps that lead to it — rows from the region's OWN
|
||||
// links, never from distance to the anchor, which is flat inside a region:
|
||||
// a handler and the store it calls are both one hop from the screen, and
|
||||
// side by side their line was a level arch, hidden at rest, so the store
|
||||
// looked wired to nothing. Longest lead-to path, settled by relaxation as
|
||||
// the order reading settles its rows; a cycle stops moving at the bound.
|
||||
const rowOf = new Map<string, number>(region.members.map((m) => [m.id, 0]));
|
||||
for (let pass = 0; pass < region.members.length; pass++) {
|
||||
let moved = false;
|
||||
for (const m of region.members) {
|
||||
const above = (parentsOf.get(m.id) ?? [])
|
||||
.map((p) => rowOf.get(p))
|
||||
.filter((x): x is number => x !== undefined);
|
||||
if (above.length === 0) continue;
|
||||
const next = Math.max(...above) + 1;
|
||||
if (next > rowOf.get(m.id)!) {
|
||||
rowOf.set(m.id, next);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
if (!moved) break;
|
||||
}
|
||||
const rows = new Map<number, WireStep[]>();
|
||||
for (const m of region.members) {
|
||||
const d = rowOf.get(m.id)!;
|
||||
rows.set(d, [...(rows.get(d) ?? []), m]);
|
||||
}
|
||||
const lines: string[][] = [];
|
||||
// The order a step was placed in, for putting its children near it.
|
||||
const placedAt = new Map<string, number>();
|
||||
let width = 0;
|
||||
for (const d of [...rows.keys()].sort((a, b) => a - b)) {
|
||||
const row = rows.get(d)!;
|
||||
const near = (s: WireStep): number => {
|
||||
const placed = (parentsOf.get(s.id) ?? []).map((p) => placedAt.get(p)).filter((x): x is number => x !== undefined);
|
||||
if (placed.length === 0) return Number.MAX_SAFE_INTEGER;
|
||||
return placed.reduce((a, b) => a + b, 0) / placed.length;
|
||||
};
|
||||
row.sort(
|
||||
(a, b) => near(a) - near(b) || (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id)
|
||||
);
|
||||
let line: string[] = [];
|
||||
let w = 0;
|
||||
for (const m of row) {
|
||||
const bw = widthOf(m.id);
|
||||
if (line.length > 0 && w + NODE_GAP + bw > REGION_LINE_MAX) {
|
||||
lines.push(line);
|
||||
width = Math.max(width, w);
|
||||
line = [];
|
||||
w = 0;
|
||||
}
|
||||
line.push(m.id);
|
||||
w += (line.length > 1 ? NODE_GAP : 0) + bw;
|
||||
placedAt.set(m.id, placedAt.size);
|
||||
}
|
||||
if (line.length > 0) {
|
||||
lines.push(line);
|
||||
width = Math.max(width, w);
|
||||
}
|
||||
}
|
||||
packed.set(region.id, { lines, width });
|
||||
}
|
||||
|
||||
// Tile the regions into bands under a width budget.
|
||||
interface Band {
|
||||
regions: Region[];
|
||||
lines: number;
|
||||
width: number;
|
||||
}
|
||||
let area = 0;
|
||||
let widest = 0;
|
||||
for (const region of regions.values()) {
|
||||
const p = packed.get(region.id)!;
|
||||
area += p.width * p.lines.length * REGION_PITCH;
|
||||
widest = Math.max(widest, p.width);
|
||||
}
|
||||
const budget = bandBudget(area, widest);
|
||||
const bands: Band[] = [];
|
||||
let band: Band | null = null;
|
||||
for (const region of regions.values()) {
|
||||
const p = packed.get(region.id)!;
|
||||
if (band === null || band.width + REGION_GUTTER + p.width > budget) {
|
||||
band = { regions: [], lines: 0, width: -REGION_GUTTER };
|
||||
bands.push(band);
|
||||
}
|
||||
band.regions.push(region);
|
||||
band.lines = Math.max(band.lines, p.lines.length);
|
||||
band.width += REGION_GUTTER + p.width;
|
||||
}
|
||||
const contentWidth = Math.max(widthOf(anchor.id), ...bands.map((b) => b.width));
|
||||
|
||||
// Place everything. The anchor is alone on top; each band's regions centre
|
||||
// as a row of columns; a region's lines centre within its own width.
|
||||
const at = new Map<string, { x: number; y: number; line: number }>();
|
||||
const zones: StepRegionZone[] = [];
|
||||
const anchorY = PADDING;
|
||||
let y = anchorY + NODE_HEIGHT + SCREEN_LAYER_GAP + BAND_GAP;
|
||||
let globalLine = 0;
|
||||
for (const b of bands) {
|
||||
let x = PADDING + (contentWidth - b.width) / 2;
|
||||
for (const region of b.regions) {
|
||||
const p = packed.get(region.id)!;
|
||||
p.lines.forEach((line, j) => {
|
||||
const lw = line.reduce((a, id) => a + widthOf(id), 0) + NODE_GAP * (line.length - 1);
|
||||
let lx = x + (p.width - lw) / 2;
|
||||
for (const id of line) {
|
||||
at.set(id, { x: lx, y: y + j * REGION_PITCH, line: globalLine + j });
|
||||
lx += widthOf(id) + NODE_GAP;
|
||||
}
|
||||
});
|
||||
zones.push({
|
||||
id: region.id,
|
||||
label: region.label,
|
||||
x,
|
||||
y,
|
||||
width: p.width,
|
||||
height: (p.lines.length - 1) * REGION_PITCH + NODE_HEIGHT,
|
||||
entry: p.lines[0]![0]!,
|
||||
});
|
||||
x += p.width + REGION_GUTTER;
|
||||
}
|
||||
y += b.lines * REGION_PITCH + BAND_GAP;
|
||||
globalLine += b.lines;
|
||||
}
|
||||
const height = y - REGION_PITCH - BAND_GAP + NODE_HEIGHT + PADDING;
|
||||
|
||||
// Layers count from the bottom, as the Map's do: the route of an edge and
|
||||
// which sides it uses fall out of the comparison alone.
|
||||
const layerOf = (id: string): number =>
|
||||
id === anchor.id ? globalLine + 1 : globalLine - (at.get(id)?.line ?? 0);
|
||||
|
||||
const nodesById = new Map<string, MapNodeLayout>();
|
||||
const place = (id: string, x: number, yy: number): void => {
|
||||
nodesById.set(id, {
|
||||
id,
|
||||
module: moduleOf.get(id)!,
|
||||
island: false,
|
||||
generated: false,
|
||||
layer: layerOf(id),
|
||||
x,
|
||||
y: yy,
|
||||
width: widthOf(id),
|
||||
height: NODE_HEIGHT,
|
||||
sourceHandles: [],
|
||||
targetHandles: [],
|
||||
ports: { top: [], bottom: [] },
|
||||
});
|
||||
};
|
||||
place(anchor.id, PADDING + (contentWidth - widthOf(anchor.id)) / 2, anchorY);
|
||||
for (const [id, p] of at) place(id, p.x, p.y);
|
||||
|
||||
// Edges and ports, exactly as the Map lays them: the route from the layers,
|
||||
// the sides from the route, the ports spread in the order the other end
|
||||
// appears left to right.
|
||||
const edges: MapEdgeLayout[] = [];
|
||||
interface SidePort extends PortRef {
|
||||
other: number;
|
||||
}
|
||||
const sidePorts = new Map<string, { top: SidePort[]; bottom: SidePort[] }>();
|
||||
const centreOf = (id: string): number => {
|
||||
const n = nodesById.get(id);
|
||||
return n ? n.x + n.width / 2 : 0;
|
||||
};
|
||||
for (const link of links) {
|
||||
const from = nodesById.get(link.source);
|
||||
const to = nodesById.get(link.target);
|
||||
if (!from || !to) continue;
|
||||
const id = linkId(link);
|
||||
const route: EdgeRoute = from.layer > to.layer ? 'down' : from.layer < to.layer ? 'up' : 'level';
|
||||
edges.push({
|
||||
id,
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
sourceHandle: `s:${id}`,
|
||||
targetHandle: `t:${id}`,
|
||||
link,
|
||||
width: strokeWidthFor(link.count),
|
||||
back: from.layer <= to.layer,
|
||||
thin: false,
|
||||
route,
|
||||
});
|
||||
const sides =
|
||||
route === 'down'
|
||||
? { source: 'bottom' as const, target: 'top' as const }
|
||||
: route === 'up'
|
||||
? { source: 'top' as const, target: 'bottom' as const }
|
||||
: { source: 'top' as const, target: 'top' as const };
|
||||
const bySide = (node: string): { top: SidePort[]; bottom: SidePort[] } => {
|
||||
const found = sidePorts.get(node) ?? { top: [], bottom: [] };
|
||||
sidePorts.set(node, found);
|
||||
return found;
|
||||
};
|
||||
bySide(link.source)[sides.source].push({ id, type: 'source', other: centreOf(link.target) });
|
||||
bySide(link.target)[sides.target].push({ id, type: 'target', other: centreOf(link.source) });
|
||||
}
|
||||
const byOther = (a: SidePort, b: SidePort): number => a.other - b.other || a.id.localeCompare(b.id);
|
||||
for (const [id, sides] of sidePorts) {
|
||||
const node = nodesById.get(id);
|
||||
if (!node) continue;
|
||||
sides.top.sort(byOther);
|
||||
sides.bottom.sort(byOther);
|
||||
node.ports = {
|
||||
top: sides.top.map((p) => ({ id: p.id, type: p.type })),
|
||||
bottom: sides.bottom.map((p) => ({ id: p.id, type: p.type })),
|
||||
};
|
||||
node.sourceHandles = sides.bottom.filter((p) => p.type === 'source').map((p) => p.id);
|
||||
node.targetHandles = sides.top.filter((p) => p.type === 'target').map((p) => p.id);
|
||||
}
|
||||
|
||||
const layout: MapLayout = {
|
||||
nodes: [...nodesById.values()],
|
||||
edges,
|
||||
layers: [],
|
||||
width: contentWidth + PADDING * 2,
|
||||
height,
|
||||
basis: { kind: 'all', declaredLinks: links.length, totalLinks: links.length },
|
||||
minWeight: 0,
|
||||
hiddenLinks: 0,
|
||||
mutual: [],
|
||||
moduleCycles: [],
|
||||
};
|
||||
return { layout, zones };
|
||||
}
|
||||
|
||||
/**
|
||||
* Which edges draw, given the selection. Selecting a step says "show me
|
||||
* everything about this one" — every line touching it comes out. At rest a
|
||||
* regioned picture hides exactly two things: the anchor's own fan — the
|
||||
* anchor leads to everything by definition, and a hundred and four ways of
|
||||
* saying so were the whole canvas, so one line into each region stands in for
|
||||
* it — and, as everywhere, what points back up the layering. Every other
|
||||
* lead-to draws, a line between two regions included: the empty state's
|
||||
* prompt firing the same handler as the header's is the picture, and hiding
|
||||
* it made a box that leads three places read as wired to nothing. A shared
|
||||
* step fed from below (the toast every handler calls) stays quiet through the
|
||||
* back rule alone. An unregioned picture keeps the Map's rule.
|
||||
*/
|
||||
export function stepEdgeVisible(model: StepsModel, edge: MapEdgeLayout, selected: string | null): boolean {
|
||||
if (selected !== null) return edge.source === selected || edge.target === selected;
|
||||
if (edge.thin || edge.back) return false;
|
||||
if (model.regions === null) return true;
|
||||
const from = model.nodes.get(edge.source)?.step;
|
||||
if (from?.anchor) return model.regionEntries?.has(edge.target) ?? true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The side panel's two lists for a selected step. */
|
||||
|
||||
@@ -762,6 +762,13 @@ export interface WireStep {
|
||||
trigger?: WireStepTrigger;
|
||||
/** The step's place in its row, in the code's order (a hop written inside another site's arguments before that site). */
|
||||
order?: number;
|
||||
/**
|
||||
* A screen anchor's picture only: the region of the screen this step belongs
|
||||
* to — the top-level component (or hook) the walk first reached it through,
|
||||
* the screen's own component for the screen body. The viewer lays a screen's
|
||||
* picture out by these; absent, the rows are distance.
|
||||
*/
|
||||
region?: { id: string; label: string };
|
||||
/**
|
||||
* For a screen or an endpoint — also a `bridge` step that is an endpoint
|
||||
* reached across a tier: its path and the symbol that serves it.
|
||||
|
||||
Reference in New Issue
Block a user