dsh-drawai 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +430 -418
- package/README.md +96 -249
- package/lib/audio-media.js +5 -2
- package/lib/client.js +45 -12
- package/lib/drawing-runner.js +6 -4
- package/lib/ffmpeg-manager.js +1056 -0
- package/lib/index.js +7719 -7396
- package/lib/motion/apply.js +4 -3
- package/lib/motion/asset-pack.js +394 -335
- package/lib/motion/assets.js +4 -3
- package/lib/motion/beats.js +3 -2
- package/lib/motion/collect.js +3 -1
- package/lib/motion/delivery.js +29 -1
- package/lib/motion/demo-assets-lock.js +10 -0
- package/lib/motion/ffmpeg.js +5 -4
- package/lib/motion/jobs.js +5 -1
- package/lib/motion/mgstyles/vendor/README.md +28 -0
- package/lib/motion/mgstyles/vendor/modules/d3/LICENSE +13 -0
- package/lib/motion/mgstyles/vendor/modules/d3/dist/d3.min.js +2 -0
- package/lib/motion/mgstyles/vendor/modules/opentype.js/LICENSE +20 -0
- package/lib/motion/mgstyles/vendor/modules/opentype.js/dist/opentype.mjs +14460 -0
- package/lib/motion/mgstyles/vendor/modules/simplex-noise/LICENSE +21 -0
- package/lib/motion/mgstyles/vendor/modules/simplex-noise/dist/esm/simplex-noise.js +466 -0
- package/lib/motion/mgstyles/vendor/modules/topojson-client/LICENSE +13 -0
- package/lib/motion/mgstyles/vendor/modules/topojson-client/dist/topojson-client.min.js +2 -0
- package/lib/motion/preview.js +13 -4
- package/lib/motion/render.js +179 -159
- package/lib/motion/runtime/package-lock.json +3058 -0
- package/lib/motion/runtime/package.json +16 -0
- package/lib/motion/style-frames.js +306 -268
- package/lib/motion/sync.js +214 -214
- package/lib/motion/video-runtime.js +192 -0
- package/lib/motion-capture.js +2 -1
- package/lib/motion-film-review.js +2 -2
- package/lib/motion-project.js +105 -79
- package/lib/motion-render-worker.js +174 -144
- package/lib/motion-vision.js +3 -2
- package/lib/routing/router.js +222 -211
- package/lib/routing/vendor/README.md +96 -96
- package/lib/routing/vendor/libavoid-routing.js +753 -753
- package/lib/routing/vendor/libavoid.min.js +59 -59
- package/lib/shotcard-reference.js +2 -2
- package/lib/style-kernel.js +16 -6
- package/lib/video-export.js +32 -20
- package/package.json +17 -12
package/lib/routing/router.js
CHANGED
|
@@ -1,211 +1,222 @@
|
|
|
1
|
-
// Shared editor adapter. Inputs/outputs are document coordinates, independent of DOM/mxGraph.
|
|
2
|
-
import { routingModeOf, styleGet, stylePatch, jettyFromStyle } from '../style-kernel.js'
|
|
3
|
-
|
|
4
|
-
const ROUTE_CLEARANCE = 16
|
|
5
|
-
const ROUTE_SPACING = 14
|
|
6
|
-
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
7
|
-
const finitePoint = (p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)
|
|
8
|
-
|
|
9
|
-
export function routingBoxes(doc) {
|
|
10
|
-
const hidden = new Set((doc.layers || []).filter((layer) => layer.visible === false).map((layer) => layer.id))
|
|
11
|
-
return doc.nodes.filter((node) => !hidden.has(node.layer)).map((node) => ({
|
|
12
|
-
id: node.id, node,
|
|
13
|
-
geo: { x: Number(node.x) || 0, y: Number(node.y) || 0, w: Number(node.w) || 170, h: Number(node.h) || 60 },
|
|
14
|
-
}))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function constraint(edge, end) {
|
|
18
|
-
const prefix = end === 'source' ? 'exit' : 'entry'
|
|
19
|
-
const x = styleGet(edge.style || '', prefix + 'X', null)
|
|
20
|
-
const y = styleGet(edge.style || '', prefix + 'Y', null)
|
|
21
|
-
if (x === null || y === null || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null
|
|
22
|
-
return { x: Number(x), y: Number(y) }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function terminal(edge, end, boxes) {
|
|
26
|
-
const id = end === 'source' ? edge.from : edge.to
|
|
27
|
-
const box = boxes.find((item) => item.id === id)
|
|
28
|
-
if (box) return box
|
|
29
|
-
const point = end === 'source' ? edge.sourcePoint : edge.targetPoint
|
|
30
|
-
return finitePoint(point) ? { id: null, geo: { ...point, w: 0, h: 0 } } : null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function border(geo, toward, pin) {
|
|
34
|
-
if (pin) return { x: geo.x + pin.x * geo.w, y: geo.y + pin.y * geo.h }
|
|
35
|
-
if (!geo.w || !geo.h) return { x: geo.x, y: geo.y }
|
|
36
|
-
const center = { x: geo.x + geo.w / 2, y: geo.y + geo.h / 2 }
|
|
37
|
-
const dx = toward.x - center.x, dy = toward.y - center.y
|
|
38
|
-
// SegmentConnector semantics: use the channel through the first/last waypoint.
|
|
39
|
-
if (toward.x >= geo.x && toward.x <= geo.x + geo.w && (toward.y <= geo.y || toward.y >= geo.y + geo.h)) {
|
|
40
|
-
return { x: toward.x, y: toward.y <= geo.y ? geo.y : geo.y + geo.h }
|
|
41
|
-
}
|
|
42
|
-
if (toward.y >= geo.y && toward.y <= geo.y + geo.h && (toward.x <= geo.x || toward.x >= geo.x + geo.w)) {
|
|
43
|
-
return { x: toward.x <= geo.x ? geo.x : geo.x + geo.w, y: toward.y }
|
|
44
|
-
}
|
|
45
|
-
if (Math.abs(dx) * geo.h >= Math.abs(dy) * geo.w) return { x: dx < 0 ? geo.x : geo.x + geo.w, y: center.y }
|
|
46
|
-
return { x: center.x, y: dy < 0 ? geo.y : geo.y + geo.h }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function storedRoute(edge, boxes) {
|
|
50
|
-
const source = terminal(edge, 'source', boxes), target = terminal(edge, 'target', boxes)
|
|
51
|
-
if (!source || !target) return null
|
|
52
|
-
const points = (edge.points || []).filter(finitePoint)
|
|
53
|
-
const center = (box) => ({ x: box.geo.x + box.geo.w / 2, y: box.geo.y + box.geo.h / 2 })
|
|
54
|
-
const a = border(source.geo, points[0] || center(target), constraint(edge, 'source'))
|
|
55
|
-
const b = border(target.geo, points[points.length - 1] || a, constraint(edge, 'target'))
|
|
56
|
-
return [a, ...points.map((p) => ({ ...p })), b].filter((p, i, all) => !i || !same(p, all[i - 1]))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function segmentHits(a, b, geo, margin = 0) {
|
|
60
|
-
return Math.min(a.x, b.x) < geo.x + geo.w + margin - 0.01 && Math.max(a.x, b.x) > geo.x - margin + 0.01 &&
|
|
61
|
-
Math.min(a.y, b.y) < geo.y + geo.h + margin - 0.01 && Math.max(a.y, b.y) > geo.y - margin + 0.01
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function obstaclesFor(edge, boxes) {
|
|
65
|
-
const ends = [terminal(edge, 'source', boxes), terminal(edge, 'target', boxes)].filter(Boolean)
|
|
66
|
-
const isContainer = (box) => styleGet(box.node?.style || '', 'container', '0') === '1' ||
|
|
67
|
-
styleGet(box.node?.style || '', 'shape', '') === 'swimlane' || boxes.some((child) => child.node?.parent === box.id)
|
|
68
|
-
return boxes.filter((box) => ends.some((end) => end.id === box.id) || !isContainer(box) || !ends.some((end) =>
|
|
69
|
-
end.geo.x >= box.geo.x && end.geo.y >= box.geo.y && end.geo.x + end.geo.w <= box.geo.x + box.geo.w && end.geo.y + end.geo.h <= box.geo.y + box.geo.h))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function validRoute(path, edge, boxes, clearance = 0) {
|
|
73
|
-
if (!path || path.length < 2 || !path.every(finitePoint)) return false
|
|
74
|
-
const obstacles = obstaclesFor(edge, boxes)
|
|
75
|
-
for (let i = 1; i < path.length; i++) {
|
|
76
|
-
const a = path[i - 1], b = path[i]
|
|
77
|
-
if (Math.abs(a.x - b.x) > 0.01 && Math.abs(a.y - b.y) > 0.01) return false
|
|
78
|
-
if (obstacles.some((box) => segmentHits(a, b, box.geo, box.id === edge.from || box.id === edge.to ? 0 : clearance))) return false
|
|
79
|
-
if (i > 1) {
|
|
80
|
-
const prev = path[i - 2]
|
|
81
|
-
if ((a.x - prev.x) * (b.x - a.x) + (a.y - prev.y) * (b.y - a.y) < -0.01) return false
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return true
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const routeLength = (path) => path.slice(1).reduce((sum, p, i) => sum + Math.abs(p.x - path[i].x) + Math.abs(p.y - path[i].y), 0)
|
|
88
|
-
const signature = (edge) => [edge.from, edge.to, edge.sourcePoint, edge.targetPoint, edge.style]
|
|
89
|
-
|
|
90
|
-
/** Incremental invalidation considers attached edges AND paths near old/new obstacles. */
|
|
91
|
-
export function affectedRoutes(before, next, oldBoxes, boxes) {
|
|
92
|
-
const old = new Map(oldBoxes.map((box) => [box.id, box]))
|
|
93
|
-
const current = new Map(boxes.map((box) => [box.id, box]))
|
|
94
|
-
const changed = new Set([...old.keys(), ...current.keys()].filter((id) => !same(old.get(id)?.geo, current.get(id)?.geo)))
|
|
95
|
-
const regions = [...changed].flatMap((id) => [old.get(id), current.get(id)].filter(Boolean))
|
|
96
|
-
const previous = new Map((before?.edges || []).map((edge) => [edge.id, edge]))
|
|
97
|
-
const hidden = new Set((next.layers || []).filter((layer) => layer.visible === false).map((layer) => layer.id))
|
|
98
|
-
const affected = new Set()
|
|
99
|
-
for (const edge of next.edges) {
|
|
100
|
-
if (routingModeOf(edge) !== 'auto' || hidden.has(edge.layer) || (edge.from && edge.from === edge.to)) continue
|
|
101
|
-
const prev = previous.get(edge.id)
|
|
102
|
-
const path = storedRoute(prev || edge, oldBoxes.length ? oldBoxes : boxes)
|
|
103
|
-
const missing = !Array.isArray(edge.points) && styleGet(edge.style || '', 'drawaiRouteStatus', null) !== 'ok'
|
|
104
|
-
if (!before && !missing && validRoute(storedRoute(edge, boxes), edge, boxes)) continue
|
|
105
|
-
if ((!before && (missing || !validRoute(storedRoute(edge, boxes), edge, boxes))) || (before && (!prev || !same(signature(prev), signature(edge)))) ||
|
|
106
|
-
changed.has(edge.from) || changed.has(edge.to) || regions.some((box) => path?.some((p, i) => i && segmentHits(path[i - 1], p, box.geo, ROUTE_CLEARANCE + 1)))) {
|
|
107
|
-
affected.add(edge.id)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Include siblings at the same terminals in one solve so their lanes can be separated.
|
|
111
|
-
const terminals = new Set(next.edges.filter((edge) => affected.has(edge.id)).flatMap((edge) => [edge.from, edge.to]).filter(Boolean))
|
|
112
|
-
if (before) for (const edge of next.edges) {
|
|
113
|
-
if (routingModeOf(edge) === 'auto' && !hidden.has(edge.layer) && edge.from !== edge.to &&
|
|
114
|
-
(terminals.has(edge.from) || terminals.has(edge.to))) affected.add(edge.id)
|
|
115
|
-
}
|
|
116
|
-
return affected
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createOrthogonalRouter(getRuntime) {
|
|
120
|
-
let solves = 0
|
|
121
|
-
function solve(edges, boxes) {
|
|
122
|
-
if (!edges.length) return {}
|
|
123
|
-
const { Avoid, AvoidRouting } = getRuntime()
|
|
124
|
-
if (!Avoid?.Router || !AvoidRouting?.computeRoutes) throw new Error('自动避障引擎未就绪')
|
|
125
|
-
const vertices = boxes.map((box) => ({ id: box.id, ...box.geo }))
|
|
126
|
-
const descriptors = edges.map((edge) => {
|
|
127
|
-
const source = terminal(edge, 'source', boxes), target = terminal(edge, 'target', boxes)
|
|
128
|
-
const pin = (end) => { const p = constraint(edge, end); return p && AvoidRouting.constraintForPoint(p.x, p.y) }
|
|
129
|
-
return { id: edge.id, source: source?.id ?? '__free-source:' + edge.id, target: target?.id ?? '__free-target:' + edge.id,
|
|
130
|
-
sourcePoint: source?.id ? undefined : edge.sourcePoint, targetPoint: target?.id ? undefined : edge.targetPoint,
|
|
131
|
-
sourceConstraint: pin('source'), targetConstraint: pin('target'),
|
|
132
|
-
sourceJetty: jettyFromStyle(edge.style || '', 'source') ?? 10,
|
|
133
|
-
targetJetty: jettyFromStyle(edge.style || '', 'target') ?? 10 }
|
|
134
|
-
})
|
|
135
|
-
const result = Object.create(null)
|
|
136
|
-
let pending = descriptors
|
|
137
|
-
for (const clearance of [ROUTE_CLEARANCE, 8, 4, 2]) {
|
|
138
|
-
if (!pending.length) break
|
|
139
|
-
solves++
|
|
140
|
-
const routes = AvoidRouting.computeRoutes(Avoid, vertices, pending, { shapeBufferDistance: clearance, idealNudgingDistance: ROUTE_SPACING })
|
|
141
|
-
const failed = []
|
|
142
|
-
for (const descriptor of pending) {
|
|
143
|
-
const edge = edges.find((item) => item.id === descriptor.id)
|
|
144
|
-
const points = routes[descriptor.id]
|
|
145
|
-
if (Array.isArray(points) && validRoute(storedRoute({ ...edge, points }, boxes), edge, boxes)) result[edge.id] = Array.from(points, (p) => ({ x: p.x, y: p.y }))
|
|
146
|
-
else failed.push(descriptor)
|
|
147
|
-
}
|
|
148
|
-
pending = failed
|
|
149
|
-
}
|
|
150
|
-
return result
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function update(before, next, options = {}) {
|
|
154
|
-
const boxes = options.boxes || routingBoxes(next)
|
|
155
|
-
const oldBoxes = options.beforeBoxes || (before ? routingBoxes(before) : [])
|
|
156
|
-
const affected = affectedRoutes(before, next, oldBoxes, boxes)
|
|
157
|
-
const accepted = new Set()
|
|
158
|
-
for (const edge of next.edges) {
|
|
159
|
-
const preview = edge.__routePreview
|
|
160
|
-
delete edge.__routePreview
|
|
161
|
-
if (!affected.has(edge.id)) continue
|
|
162
|
-
if (Array.isArray(preview) && preview.length >= 2) {
|
|
163
|
-
const trial = { ...edge, points: preview.slice(1, -1) }
|
|
164
|
-
if (same(storedRoute(trial, boxes), preview) && validRoute(preview, edge, boxes)) {
|
|
165
|
-
edge.points = trial.points
|
|
166
|
-
edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'ok' })
|
|
167
|
-
accepted.add(edge.id)
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// A rigid group move translates its internal routes unless an outside shape blocks them.
|
|
172
|
-
const previous = before?.edges.find((item) => item.id === edge.id)
|
|
173
|
-
if (!previous || !Array.isArray(previous.points)) continue
|
|
174
|
-
const a0 = terminal(previous, 'source', oldBoxes), b0 = terminal(previous, 'target', oldBoxes)
|
|
175
|
-
const a1 = terminal(edge, 'source', boxes), b1 = terminal(edge, 'target', boxes)
|
|
176
|
-
if (!a0 || !b0 || !a1 || !b1 || !a0.id || !b0.id || !same(signature(previous), signature(edge))) continue
|
|
177
|
-
const dx = a1.geo.x - a0.geo.x, dy = a1.geo.y - a0.geo.y
|
|
178
|
-
if ((!dx && !dy) || b1.geo.x - b0.geo.x !== dx || b1.geo.y - b0.geo.y !== dy ||
|
|
179
|
-
a0.geo.w !== a1.geo.w || a0.geo.h !== a1.geo.h || b0.geo.w !== b1.geo.w || b0.geo.h !== b1.geo.h) continue
|
|
180
|
-
const points = previous.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
|
|
181
|
-
if (validRoute(storedRoute({ ...edge, points }, boxes), edge, boxes, ROUTE_CLEARANCE)) {
|
|
182
|
-
edge.points = points
|
|
183
|
-
accepted.add(edge.id)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const edges = next.edges.filter((edge) => affected.has(edge.id) && !accepted.has(edge.id)).sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
1
|
+
// Shared editor adapter. Inputs/outputs are document coordinates, independent of DOM/mxGraph.
|
|
2
|
+
import { routingModeOf, styleGet, stylePatch, jettyFromStyle } from '../style-kernel.js'
|
|
3
|
+
|
|
4
|
+
const ROUTE_CLEARANCE = 16
|
|
5
|
+
const ROUTE_SPACING = 14
|
|
6
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
7
|
+
const finitePoint = (p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)
|
|
8
|
+
|
|
9
|
+
export function routingBoxes(doc) {
|
|
10
|
+
const hidden = new Set((doc.layers || []).filter((layer) => layer.visible === false).map((layer) => layer.id))
|
|
11
|
+
return doc.nodes.filter((node) => !hidden.has(node.layer)).map((node) => ({
|
|
12
|
+
id: node.id, node,
|
|
13
|
+
geo: { x: Number(node.x) || 0, y: Number(node.y) || 0, w: Number(node.w) || 170, h: Number(node.h) || 60 },
|
|
14
|
+
}))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function constraint(edge, end) {
|
|
18
|
+
const prefix = end === 'source' ? 'exit' : 'entry'
|
|
19
|
+
const x = styleGet(edge.style || '', prefix + 'X', null)
|
|
20
|
+
const y = styleGet(edge.style || '', prefix + 'Y', null)
|
|
21
|
+
if (x === null || y === null || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null
|
|
22
|
+
return { x: Number(x), y: Number(y) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function terminal(edge, end, boxes) {
|
|
26
|
+
const id = end === 'source' ? edge.from : edge.to
|
|
27
|
+
const box = boxes.find((item) => item.id === id)
|
|
28
|
+
if (box) return box
|
|
29
|
+
const point = end === 'source' ? edge.sourcePoint : edge.targetPoint
|
|
30
|
+
return finitePoint(point) ? { id: null, geo: { ...point, w: 0, h: 0 } } : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function border(geo, toward, pin) {
|
|
34
|
+
if (pin) return { x: geo.x + pin.x * geo.w, y: geo.y + pin.y * geo.h }
|
|
35
|
+
if (!geo.w || !geo.h) return { x: geo.x, y: geo.y }
|
|
36
|
+
const center = { x: geo.x + geo.w / 2, y: geo.y + geo.h / 2 }
|
|
37
|
+
const dx = toward.x - center.x, dy = toward.y - center.y
|
|
38
|
+
// SegmentConnector semantics: use the channel through the first/last waypoint.
|
|
39
|
+
if (toward.x >= geo.x && toward.x <= geo.x + geo.w && (toward.y <= geo.y || toward.y >= geo.y + geo.h)) {
|
|
40
|
+
return { x: toward.x, y: toward.y <= geo.y ? geo.y : geo.y + geo.h }
|
|
41
|
+
}
|
|
42
|
+
if (toward.y >= geo.y && toward.y <= geo.y + geo.h && (toward.x <= geo.x || toward.x >= geo.x + geo.w)) {
|
|
43
|
+
return { x: toward.x <= geo.x ? geo.x : geo.x + geo.w, y: toward.y }
|
|
44
|
+
}
|
|
45
|
+
if (Math.abs(dx) * geo.h >= Math.abs(dy) * geo.w) return { x: dx < 0 ? geo.x : geo.x + geo.w, y: center.y }
|
|
46
|
+
return { x: center.x, y: dy < 0 ? geo.y : geo.y + geo.h }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function storedRoute(edge, boxes) {
|
|
50
|
+
const source = terminal(edge, 'source', boxes), target = terminal(edge, 'target', boxes)
|
|
51
|
+
if (!source || !target) return null
|
|
52
|
+
const points = (edge.points || []).filter(finitePoint)
|
|
53
|
+
const center = (box) => ({ x: box.geo.x + box.geo.w / 2, y: box.geo.y + box.geo.h / 2 })
|
|
54
|
+
const a = border(source.geo, points[0] || center(target), constraint(edge, 'source'))
|
|
55
|
+
const b = border(target.geo, points[points.length - 1] || a, constraint(edge, 'target'))
|
|
56
|
+
return [a, ...points.map((p) => ({ ...p })), b].filter((p, i, all) => !i || !same(p, all[i - 1]))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function segmentHits(a, b, geo, margin = 0) {
|
|
60
|
+
return Math.min(a.x, b.x) < geo.x + geo.w + margin - 0.01 && Math.max(a.x, b.x) > geo.x - margin + 0.01 &&
|
|
61
|
+
Math.min(a.y, b.y) < geo.y + geo.h + margin - 0.01 && Math.max(a.y, b.y) > geo.y - margin + 0.01
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function obstaclesFor(edge, boxes) {
|
|
65
|
+
const ends = [terminal(edge, 'source', boxes), terminal(edge, 'target', boxes)].filter(Boolean)
|
|
66
|
+
const isContainer = (box) => styleGet(box.node?.style || '', 'container', '0') === '1' ||
|
|
67
|
+
styleGet(box.node?.style || '', 'shape', '') === 'swimlane' || boxes.some((child) => child.node?.parent === box.id)
|
|
68
|
+
return boxes.filter((box) => ends.some((end) => end.id === box.id) || !isContainer(box) || !ends.some((end) =>
|
|
69
|
+
end.geo.x >= box.geo.x && end.geo.y >= box.geo.y && end.geo.x + end.geo.w <= box.geo.x + box.geo.w && end.geo.y + end.geo.h <= box.geo.y + box.geo.h))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validRoute(path, edge, boxes, clearance = 0) {
|
|
73
|
+
if (!path || path.length < 2 || !path.every(finitePoint)) return false
|
|
74
|
+
const obstacles = obstaclesFor(edge, boxes)
|
|
75
|
+
for (let i = 1; i < path.length; i++) {
|
|
76
|
+
const a = path[i - 1], b = path[i]
|
|
77
|
+
if (Math.abs(a.x - b.x) > 0.01 && Math.abs(a.y - b.y) > 0.01) return false
|
|
78
|
+
if (obstacles.some((box) => segmentHits(a, b, box.geo, box.id === edge.from || box.id === edge.to ? 0 : clearance))) return false
|
|
79
|
+
if (i > 1) {
|
|
80
|
+
const prev = path[i - 2]
|
|
81
|
+
if ((a.x - prev.x) * (b.x - a.x) + (a.y - prev.y) * (b.y - a.y) < -0.01) return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const routeLength = (path) => path.slice(1).reduce((sum, p, i) => sum + Math.abs(p.x - path[i].x) + Math.abs(p.y - path[i].y), 0)
|
|
88
|
+
const signature = (edge) => [edge.from, edge.to, edge.sourcePoint, edge.targetPoint, edge.style]
|
|
89
|
+
|
|
90
|
+
/** Incremental invalidation considers attached edges AND paths near old/new obstacles. */
|
|
91
|
+
export function affectedRoutes(before, next, oldBoxes, boxes) {
|
|
92
|
+
const old = new Map(oldBoxes.map((box) => [box.id, box]))
|
|
93
|
+
const current = new Map(boxes.map((box) => [box.id, box]))
|
|
94
|
+
const changed = new Set([...old.keys(), ...current.keys()].filter((id) => !same(old.get(id)?.geo, current.get(id)?.geo)))
|
|
95
|
+
const regions = [...changed].flatMap((id) => [old.get(id), current.get(id)].filter(Boolean))
|
|
96
|
+
const previous = new Map((before?.edges || []).map((edge) => [edge.id, edge]))
|
|
97
|
+
const hidden = new Set((next.layers || []).filter((layer) => layer.visible === false).map((layer) => layer.id))
|
|
98
|
+
const affected = new Set()
|
|
99
|
+
for (const edge of next.edges) {
|
|
100
|
+
if (routingModeOf(edge) !== 'auto' || hidden.has(edge.layer) || (edge.from && edge.from === edge.to)) continue
|
|
101
|
+
const prev = previous.get(edge.id)
|
|
102
|
+
const path = storedRoute(prev || edge, oldBoxes.length ? oldBoxes : boxes)
|
|
103
|
+
const missing = !Array.isArray(edge.points) && styleGet(edge.style || '', 'drawaiRouteStatus', null) !== 'ok'
|
|
104
|
+
if (!before && !missing && validRoute(storedRoute(edge, boxes), edge, boxes)) continue
|
|
105
|
+
if ((!before && (missing || !validRoute(storedRoute(edge, boxes), edge, boxes))) || (before && (!prev || !same(signature(prev), signature(edge)))) ||
|
|
106
|
+
changed.has(edge.from) || changed.has(edge.to) || regions.some((box) => path?.some((p, i) => i && segmentHits(path[i - 1], p, box.geo, ROUTE_CLEARANCE + 1)))) {
|
|
107
|
+
affected.add(edge.id)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Include siblings at the same terminals in one solve so their lanes can be separated.
|
|
111
|
+
const terminals = new Set(next.edges.filter((edge) => affected.has(edge.id)).flatMap((edge) => [edge.from, edge.to]).filter(Boolean))
|
|
112
|
+
if (before) for (const edge of next.edges) {
|
|
113
|
+
if (routingModeOf(edge) === 'auto' && !hidden.has(edge.layer) && edge.from !== edge.to &&
|
|
114
|
+
(terminals.has(edge.from) || terminals.has(edge.to))) affected.add(edge.id)
|
|
115
|
+
}
|
|
116
|
+
return affected
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createOrthogonalRouter(getRuntime) {
|
|
120
|
+
let solves = 0
|
|
121
|
+
function solve(edges, boxes) {
|
|
122
|
+
if (!edges.length) return {}
|
|
123
|
+
const { Avoid, AvoidRouting } = getRuntime()
|
|
124
|
+
if (!Avoid?.Router || !AvoidRouting?.computeRoutes) throw new Error('自动避障引擎未就绪')
|
|
125
|
+
const vertices = boxes.map((box) => ({ id: box.id, ...box.geo }))
|
|
126
|
+
const descriptors = edges.map((edge) => {
|
|
127
|
+
const source = terminal(edge, 'source', boxes), target = terminal(edge, 'target', boxes)
|
|
128
|
+
const pin = (end) => { const p = constraint(edge, end); return p && AvoidRouting.constraintForPoint(p.x, p.y) }
|
|
129
|
+
return { id: edge.id, source: source?.id ?? '__free-source:' + edge.id, target: target?.id ?? '__free-target:' + edge.id,
|
|
130
|
+
sourcePoint: source?.id ? undefined : edge.sourcePoint, targetPoint: target?.id ? undefined : edge.targetPoint,
|
|
131
|
+
sourceConstraint: pin('source'), targetConstraint: pin('target'),
|
|
132
|
+
sourceJetty: jettyFromStyle(edge.style || '', 'source') ?? 10,
|
|
133
|
+
targetJetty: jettyFromStyle(edge.style || '', 'target') ?? 10 }
|
|
134
|
+
})
|
|
135
|
+
const result = Object.create(null)
|
|
136
|
+
let pending = descriptors
|
|
137
|
+
for (const clearance of [ROUTE_CLEARANCE, 8, 4, 2]) {
|
|
138
|
+
if (!pending.length) break
|
|
139
|
+
solves++
|
|
140
|
+
const routes = AvoidRouting.computeRoutes(Avoid, vertices, pending, { shapeBufferDistance: clearance, idealNudgingDistance: ROUTE_SPACING })
|
|
141
|
+
const failed = []
|
|
142
|
+
for (const descriptor of pending) {
|
|
143
|
+
const edge = edges.find((item) => item.id === descriptor.id)
|
|
144
|
+
const points = routes[descriptor.id]
|
|
145
|
+
if (Array.isArray(points) && validRoute(storedRoute({ ...edge, points }, boxes), edge, boxes)) result[edge.id] = Array.from(points, (p) => ({ x: p.x, y: p.y }))
|
|
146
|
+
else failed.push(descriptor)
|
|
147
|
+
}
|
|
148
|
+
pending = failed
|
|
149
|
+
}
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function update(before, next, options = {}) {
|
|
154
|
+
const boxes = options.boxes || routingBoxes(next)
|
|
155
|
+
const oldBoxes = options.beforeBoxes || (before ? routingBoxes(before) : [])
|
|
156
|
+
const affected = affectedRoutes(before, next, oldBoxes, boxes)
|
|
157
|
+
const accepted = new Set()
|
|
158
|
+
for (const edge of next.edges) {
|
|
159
|
+
const preview = edge.__routePreview
|
|
160
|
+
delete edge.__routePreview
|
|
161
|
+
if (!affected.has(edge.id)) continue
|
|
162
|
+
if (Array.isArray(preview) && preview.length >= 2) {
|
|
163
|
+
const trial = { ...edge, points: preview.slice(1, -1) }
|
|
164
|
+
if (same(storedRoute(trial, boxes), preview) && validRoute(preview, edge, boxes)) {
|
|
165
|
+
edge.points = trial.points
|
|
166
|
+
edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'ok' })
|
|
167
|
+
accepted.add(edge.id)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// A rigid group move translates its internal routes unless an outside shape blocks them.
|
|
172
|
+
const previous = before?.edges.find((item) => item.id === edge.id)
|
|
173
|
+
if (!previous || !Array.isArray(previous.points)) continue
|
|
174
|
+
const a0 = terminal(previous, 'source', oldBoxes), b0 = terminal(previous, 'target', oldBoxes)
|
|
175
|
+
const a1 = terminal(edge, 'source', boxes), b1 = terminal(edge, 'target', boxes)
|
|
176
|
+
if (!a0 || !b0 || !a1 || !b1 || !a0.id || !b0.id || !same(signature(previous), signature(edge))) continue
|
|
177
|
+
const dx = a1.geo.x - a0.geo.x, dy = a1.geo.y - a0.geo.y
|
|
178
|
+
if ((!dx && !dy) || b1.geo.x - b0.geo.x !== dx || b1.geo.y - b0.geo.y !== dy ||
|
|
179
|
+
a0.geo.w !== a1.geo.w || a0.geo.h !== a1.geo.h || b0.geo.w !== b1.geo.w || b0.geo.h !== b1.geo.h) continue
|
|
180
|
+
const points = previous.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
|
|
181
|
+
if (validRoute(storedRoute({ ...edge, points }, boxes), edge, boxes, ROUTE_CLEARANCE)) {
|
|
182
|
+
edge.points = points
|
|
183
|
+
accepted.add(edge.id)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const edges = next.edges.filter((edge) => affected.has(edge.id) && !accepted.has(edge.id)).sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
|
187
|
+
// 引擎是懒加载的外部文件(src/routing/vendor/*,从 git 装的包可能缺这一份):它整体不可用时
|
|
188
|
+
// solve() 会抛 ENOENT 或「自动避障引擎未就绪」。这条异常不能冒到写回路径上 —— 那会让整次
|
|
189
|
+
// diagram_apply 失败、一个字节都不写(issue #13)。降级成「这次没有自动路线」:已接受的
|
|
190
|
+
// 手工路线 / 刚性平移照常保留,其余边保持原有折点,由调用方在 notes 里说明。
|
|
191
|
+
let routes = {}
|
|
192
|
+
let unavailable
|
|
193
|
+
try {
|
|
194
|
+
routes = solve(edges, boxes)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
unavailable = error instanceof Error ? error.message : String(error)
|
|
197
|
+
}
|
|
198
|
+
const failed = [], routed = [...accepted]
|
|
199
|
+
if (unavailable !== undefined) return { affected: [...affected], routed, failed, solves, unavailable }
|
|
200
|
+
for (const edge of edges) {
|
|
201
|
+
const points = routes[edge.id]
|
|
202
|
+
const candidate = Array.isArray(points) ? { ...edge, points } : null
|
|
203
|
+
const path = candidate && storedRoute(candidate, boxes)
|
|
204
|
+
if (validRoute(path, edge, boxes)) {
|
|
205
|
+
// Keep a legal existing corridor for small gains, independent of frame rate.
|
|
206
|
+
const previous = before?.edges.find((item) => item.id === edge.id)
|
|
207
|
+
const oldPath = previous && storedRoute(previous, boxes)
|
|
208
|
+
if (edges.length === 1 && previous && same(signature(previous), signature(edge)) && validRoute(oldPath, edge, boxes, 2) &&
|
|
209
|
+
oldPath.length <= path.length && routeLength(oldPath) <= routeLength(path) + 16) {
|
|
210
|
+
edge.points = (previous.points || []).map((p) => ({ ...p }))
|
|
211
|
+
} else edge.points = points.map((p) => ({ ...p }))
|
|
212
|
+
edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'ok' })
|
|
213
|
+
routed.push(edge.id)
|
|
214
|
+
} else {
|
|
215
|
+
edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'blocked' })
|
|
216
|
+
failed.push(edge.id)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { affected: [...affected], routed, failed, solves }
|
|
220
|
+
}
|
|
221
|
+
return { update, solve, get solves() { return solves } }
|
|
222
|
+
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
# libavoid (pure-JS, built from source)
|
|
2
|
-
|
|
3
|
-
Obstacle-avoiding **orthogonal edge routing** for the Arrange > Layout >
|
|
4
|
-
**Orthogonal Routing** menu item (and the `orthogonalEdge` childLayout).
|
|
5
|
-
libavoid never moves a vertex — it only computes edge paths that route around
|
|
6
|
-
the vertices as obstacles.
|
|
7
|
-
|
|
8
|
-
The bundle here is **built from source** in the sibling repo
|
|
9
|
-
[`../drawio-libavoid`](https://github.com/jgraph/drawio-libavoid) — the same
|
|
10
|
-
pattern as `drawio-elk` / `drawio-mermaid`, down to the refresh path
|
|
11
|
-
(`ant bundles`) and being a **single, self-contained, self-publishing** bundle
|
|
12
|
-
loaded via one `<script>` tag. That repo compiles the published
|
|
13
|
-
[libavoid](https://www.adaptagrams.org/) C++ (from the Adaptagrams submodule,
|
|
14
|
-
LGPL-2.1) with a small draw.io-authored **embind** binding into one pure-JS file
|
|
15
|
-
(Emscripten wasm2js, `-sWASM=0`). Like `drawio-mermaid` publishes
|
|
16
|
-
`mxMermaidToDrawio`, this bundle publishes `window.Avoid` itself on load — there
|
|
17
|
-
is **no separate loader file** and no bespoke vendor script.
|
|
18
|
-
|
|
19
|
-
**No WebAssembly, no dynamic loading.** Earlier versions shipped a WASM binary
|
|
20
|
-
(base64-inlined, decoded and instantiated asynchronously). That is gone:
|
|
21
|
-
|
|
22
|
-
- **CSP**: pure JS built with `-sDYNAMIC_EXECUTION=0` needs **neither
|
|
23
|
-
`'unsafe-eval'` nor `'wasm-unsafe-eval'`** — it runs under any `script-src`.
|
|
24
|
-
- **Synchronous init**: built with `-sWASM_ASYNC_COMPILATION=0`, so
|
|
25
|
-
`initAvoidModule(mod)` populates the embind classes on `mod` before it
|
|
26
|
-
returns. The bundle's own bootstrap (baked in via `--extern-post-js`) calls it
|
|
27
|
-
and publishes `globalThis.Avoid` in one tick — no promise to await, no CSP
|
|
28
|
-
probe, no availability race, no separate loader.
|
|
29
|
-
|
|
30
|
-
The tradeoff is size/speed (pure JS is ~2–4× the old wasm and slower per solve),
|
|
31
|
-
which is acceptable: routing runs on user gestures, not a hot loop, and
|
|
32
|
-
universal CSP compatibility is the goal.
|
|
33
|
-
|
|
34
|
-
## Artifacts
|
|
35
|
-
|
|
36
|
-
| File | Origin | Purpose |
|
|
37
|
-
|---|---|---|
|
|
38
|
-
| `libavoid.min.js` | **copied** from `../drawio-libavoid/dist/libavoid.js` (by `ant bundles`) | The pure-JS module (wasm2js, embind, `DYNAMIC_EXECUTION=0`, synchronous). Self-publishing: on load it calls its own `initAvoidModule` factory synchronously, sets `window.Avoid` (and `window.__libavoidReady = Promise.resolve(Avoid)` for back-compat), and installs the `printErr` filter (demotes libavoid's non-actionable "skipping checkpoint" stderr to `console.debug`). Compiled **exception-free**: a C++ throw would abort the module and kill routing for the session, so the build patches VPSC's internal throw/catch into explicit control flow and defines `-DNDEBUG` — see `../drawio-libavoid/build/patches/`. |
|
|
39
|
-
| `libavoid-routing.js` | **hand-authored** | The shared routing core, `globalThis.AvoidRouting`: `computeRoutes` (obstacle-avoiding solve incl. fixed-connection-point pins and jettySize stub checkpoints) plus the pure geometry helpers (`constraintForPoint`, `jettyStub`, `filterEnclosing`, `insideAny`, `dirForPoint`, `clamp01`). **Canonical source** — drawio-mcp vendors verbatim copies (see Algorithm sync). |
|
|
40
|
-
| `LICENSE` | upstream | LGPL-2.1 — shipped for attribution (libavoid is LGPL). |
|
|
41
|
-
|
|
42
|
-
The editor adapter lives next to the other layouts:
|
|
43
|
-
[`js/diagramly/LibavoidRouting.js`](../diagramly/LibavoidRouting.js). It reads
|
|
44
|
-
`window.Avoid` directly (always set at load). The routing UI entry points gate
|
|
45
|
-
only on `typeof LibavoidRouting !== 'undefined'` — i.e. whether the extensions
|
|
46
|
-
bundle is loaded (a no-op in viewers). There is no availability/CSP probe: the
|
|
47
|
-
bundle is pure JS and can't be blocked by a CSP, so the old `isSupported()`
|
|
48
|
-
helper and the wasm-unsafe-eval fallback are gone.
|
|
49
|
-
|
|
50
|
-
## Load order (fixed)
|
|
51
|
-
|
|
52
|
-
`libavoid.min.js` → `libavoid-routing.js` → `LibavoidRouting.js`. Enforced in
|
|
53
|
-
`Devel.js` (dev) and the `extensions.min.js` concat in
|
|
54
|
-
`etc/build/github-build.xml` (prod). (`libavoid-routing.js` itself has no
|
|
55
|
-
load-order dependency — every entry point takes the `Avoid` namespace as a
|
|
56
|
-
parameter — it is simply kept in the block.) libavoid flows transitively into
|
|
57
|
-
`atlas.min.js`, `integrate.min.js`, and the desktop build because they include
|
|
58
|
-
`extensions.min.js` whole.
|
|
59
|
-
|
|
60
|
-
## Refreshing
|
|
61
|
-
|
|
62
|
-
Exactly like `drawio-elk` / `drawio-mermaid` — via the internal `bundles` Ant
|
|
63
|
-
target, which rebuilds all three sibling bundles and copies them in:
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
cd drawio-dev/etc/build
|
|
67
|
-
ant bundles # runs `npm run build` in each sibling, copies the outputs
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
libavoid's `npm run build` needs the Emscripten SDK (the other two need only
|
|
71
|
-
Node). `libavoid-routing.js` and this README are hand-authored — not copied. The
|
|
72
|
-
C++ version is pinned by the adaptagrams submodule commit in `../drawio-libavoid`.
|
|
73
|
-
|
|
74
|
-
## Algorithm sync
|
|
75
|
-
|
|
76
|
-
`libavoid-routing.js` here is the **canonical** routing core. It reaches its
|
|
77
|
-
consumers through the viewer.diagrams.net CDN whenever a draw.io release
|
|
78
|
-
ships this directory
|
|
79
|
-
(`https://viewer.diagrams.net/js/libavoid-js/libavoid-routing.js`):
|
|
80
|
-
|
|
81
|
-
- The **drawio-mcp app server** loads it straight from the CDN in its
|
|
82
|
-
sandboxed iframe (ETag-versioned URLs,
|
|
83
|
-
`mcp-app-server/src/libavoid-versions.js`) — no vendored copy.
|
|
84
|
-
- The **drawio-mcp tool server** (node-side pass) loads the CURRENT core
|
|
85
|
-
through an ETag-revalidated per-user disk cache
|
|
86
|
-
(`mcp-tool-server/src/routing-core-cache.js`, revalidated against the CDN
|
|
87
|
-
once per process), so routing fixes land there automatically too. Its
|
|
88
|
-
vendored `mcp-tool-server/vendor/libavoid/libavoid-routing.js` is a
|
|
89
|
-
verbatim copy kept only as the cold-cache/offline/pre-release fallback —
|
|
90
|
-
refresh it when this file changes (lagging between releases is expected;
|
|
91
|
-
the runtime cache wins whenever the CDN is reachable).
|
|
92
|
-
|
|
93
|
-
Only the routing core is shared — never loaders: every `AvoidRouting` entry
|
|
94
|
-
point takes the `Avoid` namespace as a parameter, so each consumer
|
|
95
|
-
loads/publishes `Avoid` its own way (this repo synchronously via the pure-JS
|
|
96
|
-
bundle; the mcp tool server via its vendored node WASM build).
|
|
1
|
+
# libavoid (pure-JS, built from source)
|
|
2
|
+
|
|
3
|
+
Obstacle-avoiding **orthogonal edge routing** for the Arrange > Layout >
|
|
4
|
+
**Orthogonal Routing** menu item (and the `orthogonalEdge` childLayout).
|
|
5
|
+
libavoid never moves a vertex — it only computes edge paths that route around
|
|
6
|
+
the vertices as obstacles.
|
|
7
|
+
|
|
8
|
+
The bundle here is **built from source** in the sibling repo
|
|
9
|
+
[`../drawio-libavoid`](https://github.com/jgraph/drawio-libavoid) — the same
|
|
10
|
+
pattern as `drawio-elk` / `drawio-mermaid`, down to the refresh path
|
|
11
|
+
(`ant bundles`) and being a **single, self-contained, self-publishing** bundle
|
|
12
|
+
loaded via one `<script>` tag. That repo compiles the published
|
|
13
|
+
[libavoid](https://www.adaptagrams.org/) C++ (from the Adaptagrams submodule,
|
|
14
|
+
LGPL-2.1) with a small draw.io-authored **embind** binding into one pure-JS file
|
|
15
|
+
(Emscripten wasm2js, `-sWASM=0`). Like `drawio-mermaid` publishes
|
|
16
|
+
`mxMermaidToDrawio`, this bundle publishes `window.Avoid` itself on load — there
|
|
17
|
+
is **no separate loader file** and no bespoke vendor script.
|
|
18
|
+
|
|
19
|
+
**No WebAssembly, no dynamic loading.** Earlier versions shipped a WASM binary
|
|
20
|
+
(base64-inlined, decoded and instantiated asynchronously). That is gone:
|
|
21
|
+
|
|
22
|
+
- **CSP**: pure JS built with `-sDYNAMIC_EXECUTION=0` needs **neither
|
|
23
|
+
`'unsafe-eval'` nor `'wasm-unsafe-eval'`** — it runs under any `script-src`.
|
|
24
|
+
- **Synchronous init**: built with `-sWASM_ASYNC_COMPILATION=0`, so
|
|
25
|
+
`initAvoidModule(mod)` populates the embind classes on `mod` before it
|
|
26
|
+
returns. The bundle's own bootstrap (baked in via `--extern-post-js`) calls it
|
|
27
|
+
and publishes `globalThis.Avoid` in one tick — no promise to await, no CSP
|
|
28
|
+
probe, no availability race, no separate loader.
|
|
29
|
+
|
|
30
|
+
The tradeoff is size/speed (pure JS is ~2–4× the old wasm and slower per solve),
|
|
31
|
+
which is acceptable: routing runs on user gestures, not a hot loop, and
|
|
32
|
+
universal CSP compatibility is the goal.
|
|
33
|
+
|
|
34
|
+
## Artifacts
|
|
35
|
+
|
|
36
|
+
| File | Origin | Purpose |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `libavoid.min.js` | **copied** from `../drawio-libavoid/dist/libavoid.js` (by `ant bundles`) | The pure-JS module (wasm2js, embind, `DYNAMIC_EXECUTION=0`, synchronous). Self-publishing: on load it calls its own `initAvoidModule` factory synchronously, sets `window.Avoid` (and `window.__libavoidReady = Promise.resolve(Avoid)` for back-compat), and installs the `printErr` filter (demotes libavoid's non-actionable "skipping checkpoint" stderr to `console.debug`). Compiled **exception-free**: a C++ throw would abort the module and kill routing for the session, so the build patches VPSC's internal throw/catch into explicit control flow and defines `-DNDEBUG` — see `../drawio-libavoid/build/patches/`. |
|
|
39
|
+
| `libavoid-routing.js` | **hand-authored** | The shared routing core, `globalThis.AvoidRouting`: `computeRoutes` (obstacle-avoiding solve incl. fixed-connection-point pins and jettySize stub checkpoints) plus the pure geometry helpers (`constraintForPoint`, `jettyStub`, `filterEnclosing`, `insideAny`, `dirForPoint`, `clamp01`). **Canonical source** — drawio-mcp vendors verbatim copies (see Algorithm sync). |
|
|
40
|
+
| `LICENSE` | upstream | LGPL-2.1 — shipped for attribution (libavoid is LGPL). |
|
|
41
|
+
|
|
42
|
+
The editor adapter lives next to the other layouts:
|
|
43
|
+
[`js/diagramly/LibavoidRouting.js`](../diagramly/LibavoidRouting.js). It reads
|
|
44
|
+
`window.Avoid` directly (always set at load). The routing UI entry points gate
|
|
45
|
+
only on `typeof LibavoidRouting !== 'undefined'` — i.e. whether the extensions
|
|
46
|
+
bundle is loaded (a no-op in viewers). There is no availability/CSP probe: the
|
|
47
|
+
bundle is pure JS and can't be blocked by a CSP, so the old `isSupported()`
|
|
48
|
+
helper and the wasm-unsafe-eval fallback are gone.
|
|
49
|
+
|
|
50
|
+
## Load order (fixed)
|
|
51
|
+
|
|
52
|
+
`libavoid.min.js` → `libavoid-routing.js` → `LibavoidRouting.js`. Enforced in
|
|
53
|
+
`Devel.js` (dev) and the `extensions.min.js` concat in
|
|
54
|
+
`etc/build/github-build.xml` (prod). (`libavoid-routing.js` itself has no
|
|
55
|
+
load-order dependency — every entry point takes the `Avoid` namespace as a
|
|
56
|
+
parameter — it is simply kept in the block.) libavoid flows transitively into
|
|
57
|
+
`atlas.min.js`, `integrate.min.js`, and the desktop build because they include
|
|
58
|
+
`extensions.min.js` whole.
|
|
59
|
+
|
|
60
|
+
## Refreshing
|
|
61
|
+
|
|
62
|
+
Exactly like `drawio-elk` / `drawio-mermaid` — via the internal `bundles` Ant
|
|
63
|
+
target, which rebuilds all three sibling bundles and copies them in:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
cd drawio-dev/etc/build
|
|
67
|
+
ant bundles # runs `npm run build` in each sibling, copies the outputs
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
libavoid's `npm run build` needs the Emscripten SDK (the other two need only
|
|
71
|
+
Node). `libavoid-routing.js` and this README are hand-authored — not copied. The
|
|
72
|
+
C++ version is pinned by the adaptagrams submodule commit in `../drawio-libavoid`.
|
|
73
|
+
|
|
74
|
+
## Algorithm sync
|
|
75
|
+
|
|
76
|
+
`libavoid-routing.js` here is the **canonical** routing core. It reaches its
|
|
77
|
+
consumers through the viewer.diagrams.net CDN whenever a draw.io release
|
|
78
|
+
ships this directory
|
|
79
|
+
(`https://viewer.diagrams.net/js/libavoid-js/libavoid-routing.js`):
|
|
80
|
+
|
|
81
|
+
- The **drawio-mcp app server** loads it straight from the CDN in its
|
|
82
|
+
sandboxed iframe (ETag-versioned URLs,
|
|
83
|
+
`mcp-app-server/src/libavoid-versions.js`) — no vendored copy.
|
|
84
|
+
- The **drawio-mcp tool server** (node-side pass) loads the CURRENT core
|
|
85
|
+
through an ETag-revalidated per-user disk cache
|
|
86
|
+
(`mcp-tool-server/src/routing-core-cache.js`, revalidated against the CDN
|
|
87
|
+
once per process), so routing fixes land there automatically too. Its
|
|
88
|
+
vendored `mcp-tool-server/vendor/libavoid/libavoid-routing.js` is a
|
|
89
|
+
verbatim copy kept only as the cold-cache/offline/pre-release fallback —
|
|
90
|
+
refresh it when this file changes (lagging between releases is expected;
|
|
91
|
+
the runtime cache wins whenever the CDN is reachable).
|
|
92
|
+
|
|
93
|
+
Only the routing core is shared — never loaders: every `AvoidRouting` entry
|
|
94
|
+
point takes the `Avoid` namespace as a parameter, so each consumer
|
|
95
|
+
loads/publishes `Avoid` its own way (this repo synchronously via the pure-JS
|
|
96
|
+
bundle; the mcp tool server via its vendored node WASM build).
|