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.
Files changed (45) hide show
  1. package/LICENSE +430 -418
  2. package/README.md +96 -249
  3. package/lib/audio-media.js +5 -2
  4. package/lib/client.js +45 -12
  5. package/lib/drawing-runner.js +6 -4
  6. package/lib/ffmpeg-manager.js +1056 -0
  7. package/lib/index.js +7719 -7396
  8. package/lib/motion/apply.js +4 -3
  9. package/lib/motion/asset-pack.js +394 -335
  10. package/lib/motion/assets.js +4 -3
  11. package/lib/motion/beats.js +3 -2
  12. package/lib/motion/collect.js +3 -1
  13. package/lib/motion/delivery.js +29 -1
  14. package/lib/motion/demo-assets-lock.js +10 -0
  15. package/lib/motion/ffmpeg.js +5 -4
  16. package/lib/motion/jobs.js +5 -1
  17. package/lib/motion/mgstyles/vendor/README.md +28 -0
  18. package/lib/motion/mgstyles/vendor/modules/d3/LICENSE +13 -0
  19. package/lib/motion/mgstyles/vendor/modules/d3/dist/d3.min.js +2 -0
  20. package/lib/motion/mgstyles/vendor/modules/opentype.js/LICENSE +20 -0
  21. package/lib/motion/mgstyles/vendor/modules/opentype.js/dist/opentype.mjs +14460 -0
  22. package/lib/motion/mgstyles/vendor/modules/simplex-noise/LICENSE +21 -0
  23. package/lib/motion/mgstyles/vendor/modules/simplex-noise/dist/esm/simplex-noise.js +466 -0
  24. package/lib/motion/mgstyles/vendor/modules/topojson-client/LICENSE +13 -0
  25. package/lib/motion/mgstyles/vendor/modules/topojson-client/dist/topojson-client.min.js +2 -0
  26. package/lib/motion/preview.js +13 -4
  27. package/lib/motion/render.js +179 -159
  28. package/lib/motion/runtime/package-lock.json +3058 -0
  29. package/lib/motion/runtime/package.json +16 -0
  30. package/lib/motion/style-frames.js +306 -268
  31. package/lib/motion/sync.js +214 -214
  32. package/lib/motion/video-runtime.js +192 -0
  33. package/lib/motion-capture.js +2 -1
  34. package/lib/motion-film-review.js +2 -2
  35. package/lib/motion-project.js +105 -79
  36. package/lib/motion-render-worker.js +174 -144
  37. package/lib/motion-vision.js +3 -2
  38. package/lib/routing/router.js +222 -211
  39. package/lib/routing/vendor/README.md +96 -96
  40. package/lib/routing/vendor/libavoid-routing.js +753 -753
  41. package/lib/routing/vendor/libavoid.min.js +59 -59
  42. package/lib/shotcard-reference.js +2 -2
  43. package/lib/style-kernel.js +16 -6
  44. package/lib/video-export.js +32 -20
  45. package/package.json +17 -12
@@ -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
- const routes = solve(edges, boxes)
188
- const failed = [], routed = [...accepted]
189
- for (const edge of edges) {
190
- const points = routes[edge.id]
191
- const candidate = Array.isArray(points) ? { ...edge, points } : null
192
- const path = candidate && storedRoute(candidate, boxes)
193
- if (validRoute(path, edge, boxes)) {
194
- // Keep a legal existing corridor for small gains, independent of frame rate.
195
- const previous = before?.edges.find((item) => item.id === edge.id)
196
- const oldPath = previous && storedRoute(previous, boxes)
197
- if (edges.length === 1 && previous && same(signature(previous), signature(edge)) && validRoute(oldPath, edge, boxes, 2) &&
198
- oldPath.length <= path.length && routeLength(oldPath) <= routeLength(path) + 16) {
199
- edge.points = (previous.points || []).map((p) => ({ ...p }))
200
- } else edge.points = points.map((p) => ({ ...p }))
201
- edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'ok' })
202
- routed.push(edge.id)
203
- } else {
204
- edge.style = stylePatch(edge.style || '', { drawaiRouteStatus: 'blocked' })
205
- failed.push(edge.id)
206
- }
207
- }
208
- return { affected: [...affected], routed, failed, solves }
209
- }
210
- return { update, solve, get solves() { return solves } }
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 &gt; Layout &gt;
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 &gt; Layout &gt;
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).