feat(diagram-engine): label avoidance, paired opposite edges, semantic group colours

A git-workflow flowchart rendered with no overlaps but read poorly. Three
distinct causes, each fixed and measured:

1. Edge labels sat on boxes and on each other (4 collisions on the
   reported diagram; 280 across 250 generated flowcharts). The router
   keeps LINES off the boxes but a label renders at its edge's midpoint,
   which on a long edge is beside exactly the things the line was routed
   around. placeLabels slides each label along its own edge to a clear
   spot — longest edges first, midpoint-outward tries — written as the
   geometry's relative x, which draw.io natively supports. Corpus: 280
   label collisions -> 8.

2. A->B and B->A were routed independently, so "git add" ran straight
   while "git reset" wandered through a different corridor with a kink.
   Opposite edges that agree on axis now get two absolute parallel tracks
   in the strip where the two boxes overlap, a constant 24px apart,
   converted back to port fractions. Zero crossing regressions.

3. All boxes rendered the same white, because the render layer's
   fill/stroke support was never reachable: neither add_box's schema nor
   draw_graph's nodes exposed it. Rather than exposing raw hex (the model
   picks mismatched saturations, differently every time), nodes take a
   semantic group name and the engine maps groups to a fixed palette of
   six paired fill/strokes in order of first appearance. The model names
   the zones - remote vs local vs temp - and never touches a colour.

532 unit tests pass; the 5 diagram e2e tests pass in a real browser.
This commit is contained in:
dayuan.jiang
2026-08-09 18:59:34 +09:00
parent c919a2d0ec
commit 6b5fd613f2
7 changed files with 503 additions and 5 deletions

View File

@@ -54,6 +54,16 @@ export const OperationSchema = z.discriminatedUnion("op", [
id: z.string(),
parent: z.string().optional(),
label: z.string(),
fill: z
.string()
.optional()
.describe(
"Fill colour, e.g. #DAE8FC. Prefer draw_graph's group field over picking colours",
),
stroke: z
.string()
.optional()
.describe("Border colour; pair it with fill"),
shape: z
.enum([
"box",
@@ -358,6 +368,8 @@ export function applyOperations(
...(op.shape && op.shape !== "box"
? { shape: op.shape }
: {}),
...(op.fill ? { fill: op.fill } : {}),
...(op.stroke ? { stroke: op.stroke } : {}),
...cellOf(op),
}
else if (op.op === "add_container")