mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
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:
@@ -436,6 +436,60 @@ export function routeEdges(
|
||||
}
|
||||
}
|
||||
|
||||
// --- stage 1c: pair opposite edges
|
||||
//
|
||||
// A→B and B→A are one relationship drawn as two arrows — "git add" out, "git reset"
|
||||
// back. Left to the general de-collide they land on port positions chosen for entirely
|
||||
// separate reasons at each end, so one line runs straight while its partner wanders off
|
||||
// through a different corridor. A reader expects a matched pair: two parallel lines a
|
||||
// constant gap apart, one clearly out and one clearly back.
|
||||
//
|
||||
// The two tracks are ABSOLUTE positions in the strip where the boxes overlap — the
|
||||
// corridor's centre ± half a track gap — converted back to a fraction of each box.
|
||||
// Assigning the same fraction to both boxes instead only works when they happen to be
|
||||
// the same size and aligned; on real diagrams it put the two lines 79px apart with a
|
||||
// kink in one of them.
|
||||
const seen = new Map<string, number>()
|
||||
edges.forEach((e, i) => {
|
||||
seen.set(`${e.source}|${e.target}`, i)
|
||||
})
|
||||
const PAIR_GAP = 24
|
||||
const pairedDone = new Set<number>()
|
||||
edges.forEach((e, i) => {
|
||||
if (pairedDone.has(i)) return
|
||||
const j = seen.get(`${e.target}|${e.source}`)
|
||||
if (j === undefined || j === i || pairedDone.has(j)) return
|
||||
const fi = faces[i]
|
||||
const fj = faces[j]
|
||||
const a = rects.get(e.source)
|
||||
const b = rects.get(e.target)
|
||||
if (!fi || !fj || !a || !b) return
|
||||
// Only pair edges that agree on the axis; when they disagree the geometry wants
|
||||
// them apart, and forcing them together would fight the search.
|
||||
if (fi.horiz !== fj.horiz) return
|
||||
|
||||
// The strip both boxes span, on the axis ACROSS the arrows. Two straight parallel
|
||||
// tracks need the corridor to hold them both.
|
||||
const lo = fi.horiz ? Math.max(a.y, b.y) : Math.max(a.x, b.x)
|
||||
const hi = fi.horiz
|
||||
? Math.min(a.y + a.h, b.y + b.h)
|
||||
: Math.min(a.x + a.w, b.x + b.w)
|
||||
if (hi - lo < PAIR_GAP + 12) return
|
||||
|
||||
const mid = (lo + hi) / 2
|
||||
const t1 = mid - PAIR_GAP / 2
|
||||
const t2 = mid + PAIR_GAP / 2
|
||||
const fracOf = (r: Rect, track: number) =>
|
||||
fi.horiz ? (track - r.y) / r.h : (track - r.x) / r.w
|
||||
|
||||
pairedDone.add(i)
|
||||
pairedDone.add(j)
|
||||
frac[i].s = fracOf(a, t1)
|
||||
frac[i].t = fracOf(b, t1)
|
||||
frac[j].s = fracOf(b, t2)
|
||||
frac[j].t = fracOf(a, t2)
|
||||
})
|
||||
|
||||
// --- stage 2: try shapes in order of directness, keep the first that is clear
|
||||
//
|
||||
// Segments already claimed by a routed edge, so later edges can avoid sharing a lane
|
||||
|
||||
Reference in New Issue
Block a user