import type React from 'react'; import cytoscape from 'cytoscape'; import type { Core, ElementDefinition, NodeCollection } from 'cytoscape'; import type { CanvasGraph, CanvasNode, CanvasEdge } from '../graph-projection.js'; import { type NestingKind } from '../nesting.js'; import { type FoldMembership } from '../fold-tree.js'; import { type LayoutDirection } from '../layout-direction.js'; import { type LayoutMode } from '../layout-mode.js'; import { type StylePresetId } from './style-presets.js'; import type { VisualLayoutPositions, VisualLayoutRoutes, VisualLayoutSavePayload } from '../adapters/visual/protocol-contract.js'; import { LAYER_COLORS } from '../notation/archimate.js'; export { LAYER_COLORS }; export declare function steppedZoom(current: number, direction: 1 | -1): number; /** * The closed mark vocabulary a host may hand the mounted viewer (#314, * ADR 0119). Three marks, because three is what comparison against another * model needs and a fourth waits for the demand that names it. What a mark * MEANS - added relative to what, changed since when - lives entirely on the * host's side of the seam: this canvas renders marks, it never diffs. */ export type DecorationMark = 'added' | 'removed' | 'changed'; /** * Subject id (concepts and relationships alike) to mark. The whole map is the * unit of exchange: each hand-over REPLACES the previous marks wholesale, * never merges into them, and an id the current model does not name marks * nothing, silently - the host may be describing subjects this model has not * gained (or has already lost). */ export type DecorationMap = Readonly>; /** * Marks every subject the host's decoration map names, the exact class * mechanism faults use: previous `deco-*` classes come off every element * first (replacement is wholesale, the map is the unit), then each id that * names a drawn node or edge gains its mark's class. Unknown ids are * silently inert. The `faulted` class is never touched here, and the * stylesheet orders the fault rule after the decoration rules, so a subject * both decorated and refused reads as refused. */ export declare function applyDecorations(cy: Core, decorations: DecorationMap): void; export declare function buildStylesheet(showLifecycle: boolean, showEvidence: boolean, showOwnership: boolean, showNudges: boolean, showKindLabels?: boolean, layout?: LayoutMode, stylePreset?: StylePresetId): cytoscape.StylesheetJsonBlock[]; export declare function resolveNestingParents(edges: readonly CanvasEdge[], nesting: readonly NestingKind[], coreKindOf: (id: string) => string): { readonly parentOf: ReadonlyMap; readonly consumedEdgeIds: ReadonlySet; }; export declare const liftLabel: (kindLabel: string, count: number) => string; export declare function graphToElements(graph: CanvasGraph, nesting: readonly NestingKind[], openQuestionCounts: ReadonlyMap, fold?: { /** Which instances draw folded. Empty or absent draws everything. */ readonly folded: ReadonlySet; /** From the model frame; without them only view nesting contains anything. */ readonly memberships?: readonly FoldMembership[]; /** * The subject the next question is open for (#534), from the overlay; * the node wears its count chip ringed. A folded box answers for a * member the way it does for counts. */ readonly nextQuestionSubjectId?: string | null; /** * Whether responsibility edges are drawn (#557, ADR 0159). Off, an R, C * or I edge stays out of the elements entirely: it is read in the * subject's properties, and eight lines out of one person box is not a * diagram. */ readonly showResponsibility?: boolean; }): ElementDefinition[]; export declare function filteredSubjectCount(nodes: readonly Pick[], matchedIds: readonly string[] | null, quickFilterText: string): number; export declare function applyFilter(cy: Core, matchedIds: readonly string[] | null, quickFilterText: string): void; /** The subjects the active view draws, or null when it is the whole model. */ export declare function viewNodeIds(cy: Core): ReadonlySet | null; /** * The viewport that fits `box` into a canvas of `width` x `height` below a * reserved band at the top (#533): cytoscape's own fit arithmetic, with the * band subtracted from the height and the result pushed down by it, so the * top row of a fitted graph lands under the toolbar's lower edge rather than * under the toolbar. Null when the box has no area; the caller falls back to * cytoscape's fit, which handles a single node the way it always has. */ export declare function fitViewport(box: { readonly x1: number; readonly y1: number; readonly x2: number; readonly y2: number; }, width: number, height: number, padding: number, reservedTop: number, limits: { readonly minZoom: number; readonly maxZoom: number; }): { readonly zoom: number; readonly pan: { readonly x: number; readonly y: number; }; } | null; /** * One layout run (ADR 0147): build the ELK graph for the collection, wait for * ELK, place the nodes through cytoscape's own `preset` layout - which fits * the collection and emits `layoutstop` exactly as the extension used to, so * the mount handler pins saved positions and records the framing unchanged - * and then, in a routed mode, draw ELK's routes on every edge whose two ends * are still where ELK put them. Saved positions have landed by then, so an * edge on a pinned node keeps the stylesheet's straight line. * * Every run first hands every edge back to the stylesheet: a route belongs to * the placement it was computed for, and the placement is about to change. * * Runs resolve in the order ELK finishes them, which is not the order they * were requested in when a view switch lands during a slow layout. The last * request wins: a run that finds a newer generation stamped on the instance * drops its answer, positions, routes and fit alike, rather than laying a * previous view's geometry over the current one's ids. */ /** What a saved layout holds for the active view, as the canvas honours it. */ export interface SavedLayout { readonly positions?: VisualLayoutPositions; readonly routes?: VisualLayoutRoutes; } export declare function relayoutVisible(cy: Core, direction: LayoutDirection, mode?: LayoutMode, showKindLabels?: boolean, saved?: SavedLayout): Promise; /** * Relayout after a fold change, keeping the toggled box where the reader last * saw it (#473). * * A fold changes the element set, so a fit alone will not do — the graph has to * be placed again. But the reader's eye is on the box they just clicked, and a * layout that moves it across the screen costs them their place. So: lay out, * then translate the whole result so the toggled node's screen position is the * one it had, and re-frame ONLY if that leaves the changed region off-viewport. * * The layout itself is the ORDINARY one (ADR 0121's NETWORK_SIMPLEX). ELK's * INTERACTIVE placement was proposed for this and MEASURED AND REJECTED: on the * reference Landscape it moved untouched nodes a median of 1156px on a fold and * 2799px on an unfold, against a gate of one node width, and raised crossings * where network simplex lowered them. Anchoring is what actually keeps the * reader's place; the placement strategy did not help. Table in ADR 0143. * * Returns whether it re-framed, so a caller can record a framing this actually * produced rather than one it assumed. */ export declare function relayoutAfterFold(cy: Core, direction: LayoutDirection, mode: LayoutMode, showKindLabels: boolean, anchorId: string | null, saved?: SavedLayout): Promise; export declare function fitVisible(cy: Core): boolean; export declare const DRAG_SAVE_DEBOUNCE_MS = 500; export declare function buildPositionMap(nodes: NodeCollection, inView?: ReadonlySet | null): VisualLayoutPositions; export declare function applySavedPositions(cy: Core, saved: VisualLayoutPositions | undefined): void; export declare function effectiveSavedPositions(saved: VisualLayoutPositions | undefined, viewId: string, discardedViews: ReadonlySet): VisualLayoutPositions | undefined; /** The same session-local discard, for the routes saved beside the positions. */ export declare function effectiveSavedRoutes(saved: VisualLayoutRoutes | undefined, viewId: string, discardedViews: ReadonlySet): VisualLayoutRoutes | undefined; export declare function savedLayoutInForce(saved: VisualLayoutPositions | undefined, graphNodeIds: readonly string[], matchedIds: readonly string[] | null): boolean; export declare function modelPositionOf(rendered: { readonly x: number; readonly y: number; }, pan: { readonly x: number; readonly y: number; }, zoom: number): { readonly x: number; readonly y: number; }; export interface DragSaveHandle { /** Cancels a queued save without unbinding the drag listener. */ readonly cancelPending: () => void; /** Unbinds the drag listener and cancels any queued save. */ readonly dispose: () => void; } export declare function registerDragSave(cy: Core, getActiveViewId: () => string, onSaveLayout: (payload: VisualLayoutSavePayload) => void): DragSaveHandle; interface GraphCanvasProps { readonly graph: CanvasGraph; readonly selectedId: string | null; readonly onSelect: (id: string, type: 'node' | 'edge') => void; /** * A right-click on the canvas, reported in VIEWPORT coordinates because the * menu is positioned against the window rather than against this container. * `id` is null for the background. */ readonly onContextMenu: (target: { readonly type: 'node' | 'edge' | 'canvas'; readonly id: string | null; }, position: { readonly x: number; readonly y: number; }) => void; readonly matchedIds: readonly string[] | null; readonly quickFilterText: string; /** What draws as nesting in this view, in precedence order (ADR 0101). */ readonly nesting: readonly NestingKind[]; /** Subjects a diagnostic named, marked so a failure is visible where it is. */ readonly faultedIds: ReadonlySet; /** * The host's per-subject marks (#314, ADR 0119), rendered as `deco-*` * classes the way faults are rendered. The map replaces wholesale on every * change; an empty map draws nothing, an unknown id marks nothing, and a * subject that is also faulted reads as faulted. */ readonly decorations: DecorationMap; readonly showLifecycle: boolean; readonly showEvidence: boolean; readonly showOwnership: boolean; /** Whether responsibility edges draw (#557, ADR 0159); off hides them. */ readonly showResponsibility: boolean; readonly showNudges: boolean; /** Open-question count per subject id, from the model's interrogation * overlay; an empty map (host shipped no overlay) draws no chips. */ readonly openQuestionCounts: ReadonlyMap; /** The subject the next question is open for (#534); null marks nothing. */ readonly nextQuestionSubjectId?: string | null; /** * The band at the top of the canvas the host's toolbar overlays (#533), in * CSS px from the canvas's top edge to the toolbar's lower edge plus a * margin. Every fit keeps the graph below it. Zero when nothing overlays. */ readonly reservedTop?: number; /** * Which instances draw folded, and the memberships that let the canvas know * what is inside them (#473). Absent draws everything, which is what every * caller did before folding existed. */ readonly folded?: ReadonlySet; readonly memberships?: readonly FoldMembership[]; readonly activeViewId: string; /** * Which way the active view runs its layers (#274, ADR 0121), from its * `presentation.direction`. A view that declares none is handed * `DEFAULT_DIRECTION`, so the canvas never has to decide what silence means. */ readonly direction: LayoutDirection; /** * How the active view arranges itself (ADR 0147): its `presentation.layout` * or the reviewer's pick on the canvas. A view that declares none is handed * `DEFAULT_LAYOUT` by the caller. */ readonly layout: LayoutMode; /** Whether an unnamed relationship is labelled with its reading (ADR 0147). */ readonly showKindLabels: boolean; /** Which dress the canvas wears (ADR 0148). */ readonly stylePreset: StylePresetId; /** Saved layout for the active view, or undefined when it has none yet. */ readonly savedPositions: VisualLayoutPositions | undefined; /** The routes that layout was drawing when saved (ADR 0147), if it kept any. */ readonly savedRoutes?: VisualLayoutRoutes; readonly onSaveLayout: (payload: VisualLayoutSavePayload) => void; /** * A kind dropped from the palette (#295): the kind's label and the model * position under the pointer. Optional because only a shell with a palette * has anything to drop; a host without one never sees a drop at all - the * `dragover` acceptance is gated on the callback too, so a stray drag is * left to the browser's default refusal. */ readonly onKindDrop?: (kindLabel: string, position: { readonly x: number; readonly y: number; }) => void; /** * A way to take a picture of what is drawn, handed up once the instance * exists and withdrawn when it goes. The shell holds it so a menu item can * export a PNG without reaching into cytoscape itself; `null` means there is * no canvas to photograph, which is what the menu reads to stay honest. */ readonly onCanvasReady?: (png: (() => string) | null) => void; } /** * GraphCanvas: renders a CanvasGraph using cytoscape with elk hierarchical layout. * * - Creates a cytoscape instance once on mount, destroys it on unmount * - Updates elements and reruns layout whenever the graph reference changes * - Applies layer-based coloring via the ArchiMate palette * - Wires node/edge tap handlers to call onSelect(id, type) * - Reflects selectedId prop as a visual highlight class on the matching element */ export declare function GraphCanvas({ graph, selectedId, onSelect, onContextMenu, matchedIds, quickFilterText, nesting, faultedIds, decorations, activeViewId, direction, layout, showKindLabels, stylePreset, savedPositions, savedRoutes, onSaveLayout, onKindDrop, onCanvasReady, showLifecycle, showEvidence, showOwnership, showResponsibility, showNudges, openQuestionCounts, nextQuestionSubjectId, reservedTop, folded, memberships, }: GraphCanvasProps): React.ReactElement;