feat(diagram-engine): connection vocabulary — arrowheads, parallel edges, edge ids

Arrowheads carry meaning: a crow's foot IS one-to-many, a hollow diamond IS
aggregation. The engine allowed exactly one arrowhead; this opens the
vocabulary the same way shapes were opened:

- LinkSpec gains head/tail (pass-through to endArrow/startArrow, charset-
  gated against style injection) and headFill/tailFill — fill is written
  explicitly whenever a head is declared, because UML composition and
  aggregation differ ONLY by fill and draw.io's per-head default would flip
  the meaning. bold (4px amber, for THE key relationship) included.
- Parallel edges: a second link between the same pair is allowed when it
  carries an id (ER's 'places' and 'cancels' between the same two entities);
  without one it stays an error, since two identical overlapping lines is a
  mistake. Edge ids are also what later operations address.
- Sequence messages respect a declared head (an async message's open arrow
  is UML notation) while defaulting to the solid block as before.
- draw_graph's edge schema extended to match; GraphEdge passes the new
  fields through to the link operations it generates.

5 new tests: crow's foot style emission, hollow-vs-filled round trip,
injection rejection, parallel-edge gating, bold round trip. 561 unit tests
green. ER+UML acceptance diagram (crow's foot, zero-to-one, hollow
inheritance triangle, filled composition diamond) verified in the real
editor.
This commit is contained in:
dayuan.jiang
2026-08-09 22:01:39 +09:00
parent 50826c0ac8
commit d9cdfba3e1
7 changed files with 257 additions and 6 deletions

View File

@@ -244,6 +244,22 @@ export interface LinkSpec {
label?: string
/** Dashed line — replication, sync, policy, lineage. */
dashed?: boolean
/**
* A bold arrow: the relationship IS the point — a transformation, the main flow.
* Thick and coloured, a visual element rather than a hairline connector.
*/
bold?: boolean
/**
* Arrowhead at the target / at the source. draw.io endArrow/startArrow tokens:
* block, open, diamond, diamondThin, oval, cross, ERone, ERmany, ERoneToMany,
* ERzeroToMany, ERzeroToOne, none… Unset means the default (classic at the target,
* nothing at the source). `headFill`/`tailFill` distinguish UML composition
* (filled diamond) from aggregation (hollow) — conventions where fill IS meaning.
*/
head?: string
tail?: string
headFill?: boolean
tailFill?: boolean
/** Step number, rendered as an "N. " prefix on the label. */
step?: number
/** Verbatim style, when recovered from XML. */