Plugin API addNode() bypasses the shared node placement primitives
## Problem/Motivation
`pluginApi.ts` `addNode()` computes the position for a plugin-added node with its own ad-hoc rule instead of the shared placement primitives the UI's own code paths use. A node added by a plugin therefore does not land where an equivalent manually added node would.
Split out from [#3589109](https://git.drupalcode.org/project/modeler/-/work_items/3589109), where it was flagged as "possibly worth a separate issue".
### The duplication
`ui/src/plugins/pluginApi.ts:504-522` — when the caller supplies no `position`:
```ts
const nodes = useGraphStore.getState().nodes;
let candidateX: number = LAYOUT.DEFAULT_POSITION_X;
let candidateY: number = LAYOUT.DEFAULT_POSITION_Y;
if (nodes.length > 0) {
const maxX = Math.max(...nodes.map((n) => n.position.x));
const minY = Math.min(...nodes.map((n) => n.position.y));
candidateX = maxX + LAYOUT.NODE_SPACING_X;
candidateY = minY;
}
position = findFreePosition(
{ x: candidateX, y: candidateY },
nodes,
NODE_DIMENSIONS.DEFAULT_WIDTH,
NODE_DIMENSIONS.DEFAULT_HEIGHT,
);
```
That is a single generic "right of everything, top-aligned" rule applied regardless of what kind of component is being added.
The UI instead routes through two shared primitives in `ui/src/utils/incrementalLayout.ts`:
- `computeNewEventPosition` (`:209`) — used by `handleAddEvent` in `ui/src/hooks/useNodeEdgeActions.ts:123-127`. Its own comment states it "places the new event flow to the right of all existing flows — identical behavior to auto-layout's start-node reservation logic."
- `computeSuccessorPosition` (`:128`) — used by `addSuccessorNode` in `ui/src/hooks/useQuickAdd.ts:53-57`, documented as the "same code path as auto-layout".
So both UI paths are explicitly aligned with auto-layout, and the plugin API is the one caller that is not.
### Why it matters
- **Wrong placement semantics.** `addNode()` applies event-style placement to every component type. A plugin adding an action or a gateway gets it parked to the far right at the topmost Y, rather than positioned in relation to the flow it belongs to — which is what `computeSuccessorPosition` exists to do.
- **It will drift.** `maxX + NODE_SPACING_X` is an independent reimplementation. Any future change to the shared primitives (spacing, start-node reservation, collision strategy) silently stops applying to plugin-added nodes.
- **It uses `DEFAULT_WIDTH`/`DEFAULT_HEIGHT` for collision checks** even when the node being placed is a start node, which has its own `START_NODE_WIDTH`/`START_NODE_HEIGHT` dimensions in the manual path.
## Proposed resolution
Have `pluginApi.ts` `addNode()` delegate to the same primitives as the UI:
- When the descriptor describes a start/event component, use `computeNewEventPosition` with the start-node dimensions.
- When the caller supplies a source node to attach to, use `computeSuccessorPosition`.
- Keep the explicit `descriptor.position` override as-is — a plugin that specifies a position should still win.
An explicit position and the successor case may need a small API consideration: `addNode()` currently has no notion of "add this as a successor of X", so the successor path may only become reachable once that is expressible, or it can be inferred when the plugin adds an edge in the same batch.
## Remaining tasks
- [ ] Decide how `addNode()` should express the successor case
- [ ] Route through the shared primitives
- [ ] Use the correct node dimensions per node type
- [ ] Unit test that a plugin-added node lands where the equivalent manually added node does
## User interface changes
None directly, but plugin-added components will land in more sensible positions.
---
Note: this issue was drafted by an AI agent, which verified every line reference above against `1.0.x` at `1d04d40`.
issue
GitLab AI Context
Project: project/modeler
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/modeler/-/raw/1.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/modeler
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD