207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# Editable connection routing — first slice, 2026-09-05
|
|
|
|
Status: the per-diagram route-list contract is implemented in core, source/API,
|
|
shared geometry and desktop interaction. Core/source/router checkpoints are
|
|
committed; desktop integration passed code and native acceptance at `23535b4`.
|
|
Native OA/SA routing, save/reopen, SVG and default/minimum
|
|
window trials passed. `65fa9ab` resolves the six reproduced default SA label
|
|
overlap pairs, with automated, independent and native follow-up. Final native
|
|
UI review found no material unresolved finding; see release-routing-verification.md for exact
|
|
evidence, the 119 ms dense settled-solve limitation and release boundaries.
|
|
State gesture design is preserved separately in release-state-gesture-design.md.
|
|
|
|
## Problem, scope and chosen representation
|
|
|
|
Before this slice, architecture canvas, hit testing and SVG independently
|
|
regenerated a fixed cubic between endpoints without stored edge geometry. Fit
|
|
and SVG bounds included only nodes. This prevented authored detours and could
|
|
conceal lines under cards and headers.
|
|
|
|
Use routing metadata keyed logically by (diagram, existing relation/trace ID).
|
|
One semantic edge is projected at most once per current diagram; the same edge
|
|
can have independent routing in different diagrams. A separately identified
|
|
EdgePlacement would add aliases, identity lifecycle and source declarations
|
|
without a current multiple-representation requirement. Reject desktop-only
|
|
coordinates because they lose text/API/save parity. A literal UUID-keyed map
|
|
needs a source reference-key grammar or special serializer; use a unique-keyed
|
|
list that fits existing typed records instead.
|
|
|
|
```rust
|
|
// All types derive Debug, Clone, PartialEq, Serialize, Deserialize.
|
|
// Records deny unknown fields; missing optional/default fields are accepted.
|
|
pub struct EdgeLabel {
|
|
pub fraction: f32, // [0, 1] of routed polyline arc length
|
|
pub offset: Position, // diagram-space displacement from that point
|
|
}
|
|
pub struct EdgeRoute {
|
|
pub edge: Id, // existing Relation or Trace, never a placement UUID
|
|
pub waypoints: Vec<Position>, // default []; pinned points in traversal order
|
|
pub label: Option<EdgeLabel>, // default None = automatic label placement
|
|
}
|
|
// Diagram gains: #[serde(default)] pub routes: Vec<EdgeRoute>;
|
|
Command::SetEdgeRoute { diagram: Id, route: EdgeRoute }
|
|
Command::RemoveEdgeRoute { diagram: Id, edge: Id }
|
|
```
|
|
|
|
Set upserts by edge, preserving unrelated route entries/order; Remove is
|
|
idempotent for an existing diagram/edge. Neither creates/reidentifies a semantic
|
|
object. An empty route with no label is equivalent to the automatic default;
|
|
UI uses Remove for Reset routing. Preserve explicit empty input during roundtrip.
|
|
List order has no routing meaning. Store only pinned points, not computed elbows,
|
|
endpoints, or generated label coordinates. Route edits remain one history entry.
|
|
|
|
```text
|
|
routes [
|
|
{ edge @report_exchange;
|
|
waypoints [ { x 420; y 260; }, { x 690; y 260; } ];
|
|
label { fraction 0.55; offset { x 0; y -16; } };
|
|
}
|
|
];
|
|
```
|
|
|
|
This is part of the ordinary diagram declaration. Add `edge` to language and
|
|
graphical reference catalogs; route points/label use existing typed numbers and
|
|
lists. Keep the diagram in its existing views source file. No second source
|
|
grammar or semantic relationship is introduced. Full Project JSON carries the
|
|
same list; existing POST `/api/v1/projects/{id}/transactions` accepts the two new
|
|
Command variants with existing conditional ETag behavior. Update OpenAPI and
|
|
normative reST. No new REST route is necessary.
|
|
|
|
## Shared deterministic projection and default router
|
|
|
|
Add a pure Rust core routing module, separate from semantic validation. It takes
|
|
Project, Diagram and the already projected nodes/ports/edges, returning for each
|
|
edge its full polyline, label anchor, effective pinned points and routing warnings.
|
|
Recommended seam: `route_diagram(p, d, view) -> RoutedView`, with RoutedEdge keyed
|
|
by edge ID and carrying `points: Vec<Position>`, `label: EdgeLabelGeometry` and
|
|
`warnings: Vec<RouteWarning>`. Return total bounds including paths/labels.
|
|
Desktop draw, edge/label hit tests, drag preview, Fit and SVG consume this result.
|
|
Delete the duplicate cubic and midpoint calculations after parity passes.
|
|
|
|
Default routing is orthogonal. Authored waypoints are hard points visited in
|
|
order; the router may add intermediate orthogonal elbows. Generated elbows are
|
|
not silently persisted. UI distinguishes pinned handles from computed elbows;
|
|
dragging an automatic segment/elbow pins the chosen point. Removing a pinned
|
|
point reroutes the adjacent spans; Reset removes all pins. Segment crossing and
|
|
shared runs are soft costs, node/header intersections are forbidden when a valid
|
|
route exists. Do not claim a guarantee of zero crossings for arbitrary graphs.
|
|
|
|
Use a bounded rectilinear visibility/grid search over endpoint stubs, obstacle
|
|
boundaries and pins, with Manhattan length plus bend penalty. Deterministic tie
|
|
breaking uses coordinates/direction and stable edge ID order, never random/hash
|
|
iteration. First try direct and one-/two-bend clear paths; search only as needed.
|
|
Reuse obstacle preparation within one routed view. Bound search work and report
|
|
a visible fallback warning instead of hanging or silently drawing through a card.
|
|
Cache by diagram/model revision and transient preview revision in the desktop;
|
|
pan/zoom alone does not change world geometry. Measure before adding a routing
|
|
framework or dependency. Single-edge previews retain their settled per-edge
|
|
search budget and preserve unrelated routes. A label-only edit preserves the
|
|
settled polyline and routing warnings; these invariants are implemented and
|
|
regression-tested at `30c7db1`.
|
|
|
|
## Endpoints, hierarchy and obstacles
|
|
|
|
Actual projected ports are hard endpoints. Use PortAnchorSide's outward normal
|
|
for a short orthogonal lead-out/lead-in (initial constants: 18 diagram units,
|
|
12 units clearance); a target is approached from outside along that normal.
|
|
Never move, invent or switch a function/component port to simplify routing.
|
|
Arrowheads use the final nonzero segment and existing `oriented` flag; Standard
|
|
unoriented exchanges stay unoriented. For non-port edges choose a clear side of
|
|
the projected owner rectangle, evaluating alternate sides if the cheapest is
|
|
blocked. Geometry never changes semantic direction or endpoint references.
|
|
|
|
Visible leaf cards and unrelated container interiors are obstacles. Ancestors of
|
|
either endpoint are traversable enclosures, but their header rectangles remain
|
|
obstacles; their children remain obstacles independently. The endpoint owner can
|
|
be crossed only by its own lead segment. A functional exchange may cross an
|
|
ancestor boundary without inventing a component port/allocation. Preserve the
|
|
current distinction between semantic containment and allocation projection.
|
|
|
|
If collapsing an ancestor promotes either endpoint to another visible node,
|
|
temporarily route that edge automatically. Preserve its complete stored route
|
|
and show that custom routing resumes on expansion; disable bend editing for
|
|
this promoted representation. This avoids old interior pins dragging a folded
|
|
edge through invisible child coordinates. Do not expose fictitious ports on the
|
|
collapsed container. Unrelated folding may reroute around the new obstacle but
|
|
does not suspend otherwise unchanged endpoint routes. An edge hidden because
|
|
both endpoints fold into one anchor retains its stored metadata.
|
|
|
|
## Movement, lifecycle and validation
|
|
|
|
Waypoints are absolute diagram coordinates. Moving one endpoint leaves pins in
|
|
place and reconnects the endpoint span automatically. When MoveNode moves a group
|
|
and both actual endpoint-owner placements belong to that group, translate all
|
|
its stored pins by the same delta, including hidden descendants. Label offset
|
|
stays a relative vector; arc-length anchoring moves the label with the route.
|
|
Use endpoint-owner placement lookup before visibility promotion, not the visible
|
|
collapsed anchor, to decide group membership. Other diagrams remain unchanged.
|
|
|
|
One shared pure placement-delta helper supplies this calculation to command
|
|
movement and desktop preview. Reflow currently emits multiple EditNode commands
|
|
(`diagram.rs:30`); append translated-route updates for edges whose endpoints move
|
|
by the same delta. Raw EditNode/text coordinate edits remain exact record edits
|
|
and do not implicitly move unrelated route points. Layout preserves pins and
|
|
translates them where both actual endpoint placements share a movement delta;
|
|
Reset routing is an explicit separate action instead of erasing authored work.
|
|
Reconnect/port-contract edits preserve stored pins but compute fresh endpoints;
|
|
blocked pinned geometry is reported as a recoverable layout warning.
|
|
|
|
Deleting a relation/trace removes its routes from every diagram in the same
|
|
transaction. Reidentify remaps EdgeRoute.edge without changing coordinates.
|
|
Deleting a diagram naturally removes its route list. Hiding/relocating a node,
|
|
changing projection or folding does not delete route metadata; it resumes if the
|
|
same semantic edge is shown again. Source/API may store routes for currently
|
|
hidden edges, so visibility is not a validity requirement.
|
|
|
|
Structural validation rejects duplicate edge entries, references to nonexistent
|
|
or non-edge objects, nonfinite/out-of-range coordinates, invalid label fraction,
|
|
and excessive point counts. Use existing MAX_COORDINATE. Adopt 128 pinned points
|
|
per edge for this slice; count route records in admission totals and retain
|
|
existing aggregate payload/source limits. A point inside an obstacle or an
|
|
impossible path is a located layout warning, not semantic corruption or permission
|
|
to erase the point. Route defaults/missing fields must be the same in UI, source,
|
|
JSON and REST. V1/V2 historical readers/goldens stay frozen; migration produces
|
|
empty routes. Existing schema3 files without routes load with an empty list;
|
|
this is an additive extension of the unreleased schema3 model.
|
|
|
|
## UI slice, partition and acceptance
|
|
|
|
Select a connection to reveal route handles; double-click a segment or use a
|
|
visible Add bend action, drag a handle, select a handle and Remove bend, or Reset
|
|
routing. Drag the label separately. Endpoint handles are not bend handles and
|
|
cannot detach ports. Use fixed screen-space hit radii (approximately 8 px divided
|
|
by zoom), explicit nearest-hit precedence and segment distances from the actual
|
|
polyline. Node/label/handle hit ordering must work inside nested enclosures.
|
|
Capture the initiating diagram/revision; Escape cancels; release emits one command;
|
|
stale gestures or invalid source/property drafts cannot mutate a different model.
|
|
Use the release cursor as the final gesture position even if no final motion
|
|
event was delivered; short/coalesced native drags must commit their displacement.
|
|
|
|
Core schema/lifecycle owns model.rs, session.rs, references.rs, validation.rs and
|
|
focused tests. Independent router owns new routing.rs plus geometry tests after
|
|
seam freeze. Source/API worker owns language/reST/OpenAPI and round-trip/HTTP
|
|
tests after schema freeze; coordinate with current source-project work. Lead owns
|
|
diagram.rs, app.rs, views/forms, actual CUA and integration. Core/source/router
|
|
workers have released their implementation ownership; the lead owns final native
|
|
integration. Continue partitioning any follow-up edits to avoid concurrent writers.
|
|
|
|
Automated gates: independent routes for one edge in two views; add/move/remove and
|
|
one-step exact undo; deletion/reidentify; malformed/duplicate/huge inputs;
|
|
V1/V2/schema3-default loading; source/JSON/HTTP equivalence; obstacle clearance,
|
|
port normals, nested header avoidance, deterministic routing, promoted endpoints,
|
|
group movement and bounds. Test actual pointer gestures at 50%, 100% and 150%.
|
|
Test SVG polyline/arrow/label coordinates against shared routing output.
|
|
|
|
Native gate: a nested SA architecture with function and Standard/Flow component
|
|
ports, three participants and enough exchanges to force detours. Add/move/remove
|
|
pins, drag a label, move one endpoint and a whole container, fold/unfold, undo,
|
|
save/reopen and export SVG at 1440x900 and 1100x700. Inspect crossings, headers,
|
|
labels, handles and typed port attachment. Measure interactive latency on a
|
|
100-node/150-edge fixture before claiming smoothness; initial target is below
|
|
100 ms for settled routing and no multi-second stalls during drag. Record actual
|
|
times/results, not forecasts. Code/native acceptance and exact performed scopes
|
|
are recorded in release-routing-verification.md. The measured release full solve
|
|
is 119 ms, so the initial settled target is not claimed as passed. Current Linux
|
|
verification and broader release acceptance remain separate from the completed
|
|
routing workflows.
|