import { ElementRef, EventEmitter, OnInit, WritableSignal } from '@angular/core'; import { EscalationType, Form, Workflow, WorkflowAction, WorkflowStage } from 'verben-workflow-ui/src/lib/models'; import { WorkflowDataService } from '../workflow-data.service'; import { WorkflowDesignerState } from '../workflow-designer.state'; import { Connection, ConnectionPoint, Node, SwimlaneItem, ToolType, Node as WFNode } from '../workflow-designer.types'; import * as i0 from "@angular/core"; /** * The SVG editing surface. Renders the entire scene — swimlanes, nodes, * connections, and overlaid popups — from {@link WorkflowDesignerState}, and * owns nearly all pointer interaction: placing nodes, dragging them within and * between lanes, and the connection drag/drop pipeline. * * This is the largest file in the module. It reads and mutates the shared * `state` singleton directly rather than holding its own model. * * ## Coordinate spaces used here * * - {@link getMousePosition} returns **absolute canvas** coordinates — the space * `state.addNode` and `Math.floor(y / 263)` expect. * - {@link getViewportPosition} additionally subtracts scroll offsets and is * used **only** to position HTML popups over the SVG. Never feed its output * into node or connection math. * * Read `../docs/02-coordinate-system.md` before changing anything positional. * * ## Popups * * Popup visibility is tracked with local booleans here, even though a * `PopupService` exists. Only `StageDialogComponent` uses that service; the two * mechanisms coexist. See `../docs/06-known-issues.md` section C. * * @see ../docs/04-interactions.md — the user-facing flows this component implements */ export declare class DesignerCanvasComponent implements OnInit { state: WorkflowDesignerState; private dataService; /** Active toolbar tool; `null` means pointer mode. */ selectedTool: ToolType; /** * Canvas click position in **absolute** coordinates, for the parent to act on * per the selected tool. * * Also carries a sentinel: `{ x: -1, y: laneIndex }` means "edit the swimlane * at `laneIndex`" (the lane's pencil button), not a placement. */ clickedPosition: EventEmitter<{ x: number; y: number; }>; /** Emitted when a workflow is chosen for a subflow node. */ subflowSelected: EventEmitter; /** Where a stage will be created once the stage dialog is confirmed. */ pendingStagePosition: { swimlaneIndex: number; x: number; y: number; } | null; /** A stage node's properties were edited on the canvas. */ stagePropertiesUpdated: EventEmitter<{ nodeId: string; stageData: Partial; }>; /** A connection's action properties were edited on the canvas. */ actionPropertiesUpdated: EventEmitter<{ connectionId: string; actionData: Partial; }>; /** The SVG root; its bounding box defines absolute canvas space. */ canvasRef: ElementRef; onWindowMouseUp(event: MouseEvent): void; canvasWidth: number; canvasHeight: number; gridSize: number; swimlaneHeight: number; visibleConnectionPointsNodeId: string | null; popupVisible: boolean; popupX: number; popupY: number; allowedNodeTypes: string[]; showStartNodeFormPopup: boolean; startNodeFormsList: Form[]; isLoadingStartNodeForms: boolean; startNodeFormPopupX: number; startNodeFormPopupY: number; activeNodeForFormSelection: Node | null; nodeForms: WritableSignal>; showSubflowPopup: boolean; workflowsList: Workflow[]; isLoadingWorkflows: boolean; subflowPopupX: number; subflowPopupY: number; pendingSubflowPosition: { swimlaneIndex: number; x: number; y: number; } | null; pendingDecisionConnection: { decisionNodeId: string; swimlaneIndex: number; x: number; y: number; } | null; pendingConnectionSourcePoint: ConnectionPoint | null; pendingConnectionSourceSwimlaneIndex: number | null; isDraggingNode: boolean; draggingNode: Node | null; private draggingNodeSwimlaneIndex; /** Mouse offset relative to node top-left at drag start (absolute canvas coords). */ private dragOffsetX; private dragOffsetY; showDecisionConditionDialog: boolean; showConditionsDialog: boolean; activeDecisionNodeId: string; activeConnectionId: string; decisionConditionPopupX: number; decisionConditionPopupY: number; isCreatingStageFromDecision: boolean; pendingDecisionCondition: string; activeStageId: string | null; activeActionId: string | null; showStageDialogLocal: boolean; showEscalationDialogLocal: boolean; activeEscalationType: WritableSignal; activeStageCode: WritableSignal; activeStageData: Partial; activeActionData: Connection | null; showActionDialogLocal: boolean; potentialTargetPointId: string | null; constructor(state: WorkflowDesignerState, dataService: WorkflowDataService); ngOnInit(): void; onCanvasClick(event: MouseEvent): void; onShowShieldDialog(stageId: string): void; onShowActionDialog(connectionInfo: Connection): void; onShowEscalationDialog(event: { stageId: string; type: EscalationType; }): void; onStageDialogClosed(): void; onActionDialogClosed(): void; onActionDialogDeleted(connectionId: string): void; onEscalationDialogClosed(): void; onStageDialogSaved(stageData: Partial): void; onActionDialogSaved(actionData: Connection): void; updateCanvasSize(): void; /** * Converts a DOM mouse event into **absolute canvas coordinates**, relative * to the SVG's bounding box. * * This is the space that `state.addNode`, `Math.floor(y / 263)`, and all * connection math expect. Use this — not {@link getViewportPosition} — for * anything positional. */ private getMousePosition; /** * Converts absolute canvas coordinates into **viewport coordinates** by * subtracting the scroll container's offsets. * * Used **only** to position HTML popups overlaid on the SVG. Feeding this * into node or connection math produces elements that drift as the canvas * scrolls. */ private getViewportPosition; showConnectionPoints(nodeId: string, swimlaneIndex: number): void; isMouseOverNode(event: MouseEvent, node: WFNode, swimlaneIndex: number): boolean; getNodeBoundsWithPadding(node: WFNode, swimlaneIndex: number): { x: number; y: number; width: number; height: number; }; hideConnectionPoints(nodeId: string): void; isConnectionPointVisible(nodeId: string): boolean; startConnectionDrag(event: MouseEvent, point: ConnectionPoint, swimlaneIndex: number): void; highlightPotentialTargets(event: MouseEvent): void; /** * Called from the template when the user presses the mouse button on a node. * Starts the drag interaction and registers document-level listeners so the * drag continues even when the cursor leaves the SVG canvas. */ onNodeMouseDown(event: MouseEvent, node: Node, swimlaneIndex: number): void; /** * Arrow function (preserves `this`) — runs on every mousemove while dragging * a node. Updates node.x / node.y in-place so Angular's change detection * picks it up and redraws the node and all connected paths automatically. */ onNodeDragMove: (event: MouseEvent) => void; /** Scratch fields used to pass computed values from move → end handlers. */ private _pendingTargetSwimlaneIndex; private _pendingAbsoluteY; /** * Arrow function (preserves `this`) — fires on mouseup after a node drag. * Finalises the node position and performs swimlane migration if necessary. */ onNodeDragEnd: (_event: MouseEvent) => void; /** * Returns a CSS cursor value for the node rendering. * Shows 'grabbing' while the node is being dragged, 'grab' otherwise. */ getCursorForNode(nodeId: string): string; onMouseMove: (event: MouseEvent) => void; updateConnectionDrag(currentX: number, currentY: number): void; /** * Terminates a connection drag. Three outcomes, evaluated in order: * * 1. **Dropped on an existing connection point** — if the source and target * are different nodes and `state.canConnect` allows the type pairing, the * connection is created immediately. * 2. **Dropped inside a swimlane but not on a point** — opens the node-type * popup ({@link showConnectionPopup}), offering only the types permitted by * `state.getAllowedTargetNodeTypes()`. The node is created later, by * {@link createNodeConnection}. * 3. **Dropped outside every swimlane** — the drag is cancelled. * * Always detaches the document-level move/up listeners first, so an early * return cannot leave them attached. */ onMouseUp: (event: MouseEvent) => void; /** * Hit-tests an absolute canvas position against every node's connection * points, resolving each to absolute space as * `node.y + swimlaneIndex * 263 + point.y` — the inverse of the render * formula. * * Returns the first point within `tolerance` pixels, or `null`. Scans all * lanes and all nodes linearly. * * @param x Absolute canvas X (from {@link getMousePosition}). * @param y Absolute canvas Y. * @param tolerance Hit radius in pixels. */ findConnectionPointAtPosition(x: number, y: number, tolerance?: number): { point: ConnectionPoint; nodeId: string; swimlaneIndex: number; } | null; isPositionInsideSwimlane(y: number): boolean; showConnectionPopup(x: number, y: number): void; hideConnectionPopup(): void; /** * Creates a new node at the drop point and connects the dragged edge to it — * the path taken when the user picks a type from the node-type popup. * * ## The coordinate contract this method must honour * * The target swimlane is derived from the **drop** Y * (`Math.floor(endY / 263)`, clamped to the lane range), and `endY` is passed * to `state.addNode` **absolute and unconverted**. `addNode` performs the * lane-relative conversion itself. * * This method previously did the opposite — pre-converting `endY` and passing * the *source* lane — which made `addNode` subtract the lane offset twice, so * the new stage rendered one lane too high and attached to the wrong * swimlane. `designer-canvas.connection-drop.spec.ts` guards against the * regression. Do not reintroduce a conversion here. * * Connecting a decision to a stage defers node creation until the branch * condition is supplied, via {@link pendingDecisionConnection}. */ createNodeConnection(nodeType: string): void; onDecisionConditionSaved(event: { connectionId: string; condition: string; }): void; onDecisionConditionCancelled(): void; getConnectionPath(): string; getConnectionPathForSavedConnection(connection: Connection): string; getOrthogonalPath(startX: number, startY: number, endX: number, endY: number, pointType: 'top' | 'right' | 'bottom' | 'left'): string; selectFormForStage(form: Form | null, node: Node): void; onStagePropertiesUpdated(event: { nodeId: string; stageData: Partial; }): void; onConnectionCreated(connection: Connection): void; updateNodeConnections(node: WFNode): void; onEditSwimlane(event: MouseEvent, swimlane: SwimlaneItem, swimlaneIndex: number): void; toggleStartNodeFormPopup(event: MouseEvent, nodeX: number, nodeY: number, swimlaneIndex: number, node?: Node): void; selectStartNodeForm(form: Form | null): void; showSubflowSelectionPopup(x: number, y: number, swimlaneIndex: number): void; selectSubflowWorkflow(workflow: Workflow): void; showDecisionConditionsPopup(event: MouseEvent, node: WFNode, swimlaneIndex: number): void; ensureSwimlaneInView(swimlaneIndex: number): void; getLabelPosition(connection: Connection): { x: number; y: number; }; saveConnectionLabel(connection: Connection, label: string): void; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; }