@typeonce/effect-machine-devtools 0.31.2 → 0.33.0
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/dist/MachineRegistry.d.ts +4 -3
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineRegistry.js +4 -3
- package/dist/MachineRegistry.js.map +1 -1
- package/dist/client/assets/index-2uvShdlX.js +37 -0
- package/dist/client/index.html +1 -1
- package/dist/internal/devServer.d.ts +3 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +8 -6
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/machineRegistry.js +9 -6
- package/dist/internal/machineRegistry.js.map +1 -1
- package/package.json +2 -2
- package/src/MachineRegistry.ts +4 -3
- package/src/internal/browser/chart-layout-policy.ts +2 -2
- package/src/internal/browser/chart-layout.ts +236 -51
- package/src/internal/browser/chart-model.ts +3 -0
- package/src/internal/browser/chart-renderer.ts +5 -4
- package/src/internal/browser/example-machine.ts +94 -85
- package/src/internal/browser/hierarchy-routing-example.ts +67 -58
- package/src/internal/browser/invoke-outcomes-example.ts +153 -143
- package/src/internal/browser/layout-resilience-example.ts +115 -104
- package/src/internal/browser/parallel-completion-example.ts +117 -107
- package/src/internal/browser/parallel-owner-routing-example.ts +104 -0
- package/src/internal/browser/planner-example.ts +44 -38
- package/src/internal/browser/protocol-events-example.ts +123 -102
- package/src/internal/browser/shared-terminal-routing-example.ts +125 -120
- package/src/internal/browser/text-tree.ts +2 -2
- package/src/internal/browser/transition-semantics-example.ts +143 -131
- package/src/internal/browser/visualizer-model.ts +7 -7
- package/src/internal/devServer.ts +15 -12
- package/src/internal/machineRegistry.ts +17 -17
- package/dist/client/assets/index-CSgOYHLh.js +0 -37
|
@@ -10,12 +10,23 @@ import type {
|
|
|
10
10
|
} from "elkjs/lib/elk-api.js"
|
|
11
11
|
import ELKBundle from "elkjs/lib/elk.bundled.js"
|
|
12
12
|
import { type ChartPortSide, makeChartLayoutPolicy } from "./chart-layout-policy.js"
|
|
13
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
type ChartEdge,
|
|
15
|
+
type ChartInitial,
|
|
16
|
+
type ChartModel,
|
|
17
|
+
type ChartNode,
|
|
18
|
+
type ChartRuntimeTarget,
|
|
19
|
+
isChartStateDescendant
|
|
20
|
+
} from "./chart-model.js"
|
|
14
21
|
|
|
15
22
|
export const maxVisibleActivities = 3
|
|
16
23
|
export const chartSelfLoopMinimumClearance = 24
|
|
17
24
|
export const chartEdgeLabelSpacing = 5
|
|
18
25
|
|
|
26
|
+
const initialMarkerTopOffset = 17
|
|
27
|
+
const initialMarkerSize = 7
|
|
28
|
+
const initialMarkerRouteClearance = 12
|
|
29
|
+
|
|
19
30
|
export interface ChartPoint {
|
|
20
31
|
readonly x: number
|
|
21
32
|
readonly y: number
|
|
@@ -259,8 +270,6 @@ const runtimeNodeId = (target: ChartRuntimeTarget): string => `node:${target.id}
|
|
|
259
270
|
const runtimeTargetPortId = (target: ChartRuntimeTarget): string => `port:${target.edgeId}:target`
|
|
260
271
|
const unconnectedRegionId = (parent: string | null): string => `region:unconnected:${parent ?? "root"}`
|
|
261
272
|
const isSelfTransition = (edge: ChartEdge): boolean => edge.kind === "targetless" || edge.target === edge.source
|
|
262
|
-
const isDescendantPath = (path: string, ancestor: string): boolean => path.startsWith(`${ancestor}.`)
|
|
263
|
-
|
|
264
273
|
const selfLoopsBySource = (edges: ReadonlyArray<ChartEdge>): ReadonlyMap<string, number> => {
|
|
265
274
|
const counts = new Map<string, number>()
|
|
266
275
|
for (const edge of edges) {
|
|
@@ -380,7 +389,7 @@ const makeGraph = (
|
|
|
380
389
|
id: node.path,
|
|
381
390
|
ports: [...ports.get(node.path) ?? []],
|
|
382
391
|
layoutOptions: {
|
|
383
|
-
...(profile.portConstraints === "fixed"
|
|
392
|
+
...(profile.portConstraints === "fixed"
|
|
384
393
|
? { "elk.portConstraints": "FIXED_SIDE" }
|
|
385
394
|
: {}),
|
|
386
395
|
"elk.spacing.portPort": "24",
|
|
@@ -508,6 +517,17 @@ const add = (left: ChartPoint, right: ChartPoint): ChartPoint => ({
|
|
|
508
517
|
y: left.y + right.y
|
|
509
518
|
})
|
|
510
519
|
|
|
520
|
+
const between = (value: number, first: number, second: number): boolean =>
|
|
521
|
+
value >= Math.min(first, second) && value <= Math.max(first, second)
|
|
522
|
+
|
|
523
|
+
const redundantCollinearPoint = (
|
|
524
|
+
start: ChartPoint,
|
|
525
|
+
middle: ChartPoint,
|
|
526
|
+
end: ChartPoint
|
|
527
|
+
): boolean =>
|
|
528
|
+
start.x === middle.x && middle.x === end.x && between(middle.y, start.y, end.y) ||
|
|
529
|
+
start.y === middle.y && middle.y === end.y && between(middle.x, start.x, end.x)
|
|
530
|
+
|
|
511
531
|
const compactPoints = (points: ReadonlyArray<ChartPoint>): ReadonlyArray<ChartPoint> => {
|
|
512
532
|
const result: Array<ChartPoint> = []
|
|
513
533
|
for (const point of points) {
|
|
@@ -516,8 +536,7 @@ const compactPoints = (points: ReadonlyArray<ChartPoint>): ReadonlyArray<ChartPo
|
|
|
516
536
|
const beforePrevious = result.at(-2)
|
|
517
537
|
if (
|
|
518
538
|
beforePrevious !== undefined && previous !== undefined &&
|
|
519
|
-
(beforePrevious
|
|
520
|
-
beforePrevious.y === previous.y && previous.y === point.y)
|
|
539
|
+
redundantCollinearPoint(beforePrevious, previous, point)
|
|
521
540
|
) {
|
|
522
541
|
result[result.length - 1] = point
|
|
523
542
|
} else {
|
|
@@ -589,7 +608,7 @@ const horizontalBoundaryIntersection = (
|
|
|
589
608
|
return { x: start.x <= end.x ? minimum : maximum, y }
|
|
590
609
|
}
|
|
591
610
|
|
|
592
|
-
const
|
|
611
|
+
const anchorRouteAtCompoundHeader = (
|
|
593
612
|
points: ReadonlyArray<ChartPoint>,
|
|
594
613
|
node: LaidOutChartNode
|
|
595
614
|
): ReadonlyArray<ChartPoint> => {
|
|
@@ -604,25 +623,16 @@ const trimRouteFromCompoundHeader = (
|
|
|
604
623
|
)
|
|
605
624
|
if (intersection !== null) return compactPoints([intersection, ...points.slice(index)])
|
|
606
625
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
nodes: ReadonlyMap<string, LaidOutChartNode>
|
|
614
|
-
): ReadonlyArray<ChartPoint> => {
|
|
615
|
-
if (edge.target !== null && isDescendantPath(edge.target, edge.source)) {
|
|
616
|
-
const source = nodes.get(edge.source)
|
|
617
|
-
return source === undefined ? points : trimRouteFromCompoundHeader(points, source)
|
|
618
|
-
}
|
|
619
|
-
if (edge.target !== null && isDescendantPath(edge.source, edge.target)) {
|
|
620
|
-
const target = nodes.get(edge.target)
|
|
621
|
-
return target === undefined
|
|
622
|
-
? points
|
|
623
|
-
: [...trimRouteFromCompoundHeader([...points].reverse(), target)].reverse()
|
|
626
|
+
const first = points[0]
|
|
627
|
+
const next = points[1]
|
|
628
|
+
if (first === undefined || next === undefined) return points
|
|
629
|
+
const anchor = {
|
|
630
|
+
x: Math.min(node.x + node.width, Math.max(node.x, first.x)),
|
|
631
|
+
y: boundary
|
|
624
632
|
}
|
|
625
|
-
return
|
|
633
|
+
return first.x === next.x
|
|
634
|
+
? compactPoints([anchor, ...points.slice(1)])
|
|
635
|
+
: compactPoints([anchor, { x: next.x, y: boundary }, ...points.slice(1)])
|
|
626
636
|
}
|
|
627
637
|
|
|
628
638
|
type RouteSide = "NORTH" | "EAST" | "SOUTH" | "WEST"
|
|
@@ -648,6 +658,23 @@ const endpointSide = (point: ChartPoint, node: LaidOutChartNode): RouteSide => {
|
|
|
648
658
|
return distances.sort((left, right) => left[1] - right[1])[0]![0]
|
|
649
659
|
}
|
|
650
660
|
|
|
661
|
+
const endpointStepSide = (
|
|
662
|
+
boundary: ChartPoint,
|
|
663
|
+
adjacent: ChartPoint,
|
|
664
|
+
node: LaidOutChartNode
|
|
665
|
+
): RouteSide => {
|
|
666
|
+
const rect = routeNodeRect(node)
|
|
667
|
+
if (boundary.y === adjacent.y) {
|
|
668
|
+
if (boundary.x === rect.left) return "WEST"
|
|
669
|
+
if (boundary.x === rect.right) return "EAST"
|
|
670
|
+
}
|
|
671
|
+
if (boundary.x === adjacent.x) {
|
|
672
|
+
if (boundary.y === rect.top) return "NORTH"
|
|
673
|
+
if (boundary.y === rect.bottom) return "SOUTH"
|
|
674
|
+
}
|
|
675
|
+
return endpointSide(boundary, node)
|
|
676
|
+
}
|
|
677
|
+
|
|
651
678
|
const outwardPoint = (point: ChartPoint, side: RouteSide, distance: number): ChartPoint => {
|
|
652
679
|
switch (side) {
|
|
653
680
|
case "NORTH":
|
|
@@ -741,6 +768,90 @@ const routeIsClear = (
|
|
|
741
768
|
obstacles.every((obstacle) => !segmentCrossesInterior(previous, point, obstacle))
|
|
742
769
|
})
|
|
743
770
|
|
|
771
|
+
const localDescendantRoute = (
|
|
772
|
+
edge: ChartEdge,
|
|
773
|
+
points: ReadonlyArray<ChartPoint>,
|
|
774
|
+
source: LaidOutChartNode,
|
|
775
|
+
target: LaidOutChartNode,
|
|
776
|
+
nodes: ReadonlyArray<LaidOutChartNode>,
|
|
777
|
+
lanes: Map<string, number>
|
|
778
|
+
): ReadonlyArray<ChartPoint> | null => {
|
|
779
|
+
const targetPath = edge.target
|
|
780
|
+
if (
|
|
781
|
+
targetPath === null || source.node.type !== "parallel" ||
|
|
782
|
+
endpointSide(points.at(-1)!, target) !== "NORTH"
|
|
783
|
+
) return null
|
|
784
|
+
const containers = nodes.filter((node) =>
|
|
785
|
+
node.node.children.length > 0 &&
|
|
786
|
+
node.node.path !== edge.source &&
|
|
787
|
+
node.node.path !== edge.target &&
|
|
788
|
+
isChartStateDescendant(node.node.path, edge.source) &&
|
|
789
|
+
isChartStateDescendant(targetPath, node.node.path)
|
|
790
|
+
)
|
|
791
|
+
if (containers.length === 0) return null
|
|
792
|
+
|
|
793
|
+
const sourceHeader = nodeHeaderRect(source)
|
|
794
|
+
const targetRect = routeNodeRect(target)
|
|
795
|
+
const end = pointOnNodeBoundary(points.at(-1)!, target, "NORTH")
|
|
796
|
+
const targetStub = {
|
|
797
|
+
x: end.x,
|
|
798
|
+
y: targetRect.top - initialMarkerTopOffset - initialMarkerRouteClearance
|
|
799
|
+
}
|
|
800
|
+
if (targetStub.y <= sourceHeader.bottom) return null
|
|
801
|
+
|
|
802
|
+
const containerHeaders = containers.map(nodeHeaderRect)
|
|
803
|
+
const left = Math.min(...containerHeaders.map((header) => header.left))
|
|
804
|
+
const right = Math.max(...containerHeaders.map((header) => header.right))
|
|
805
|
+
const candidates = (["left", "right"] as const).flatMap((side) => {
|
|
806
|
+
const key = `descendant:${edge.source}:${containers[0]!.node.path}:${side}`
|
|
807
|
+
const lane = lanes.get(key) ?? 0
|
|
808
|
+
const x = side === "left" ? left - 12 - lane * 8 : right + 12 + lane * 8
|
|
809
|
+
if (x < sourceHeader.left || x > sourceHeader.right) return []
|
|
810
|
+
const route = compactPoints([
|
|
811
|
+
{ x, y: sourceHeader.bottom },
|
|
812
|
+
{ x, y: targetStub.y },
|
|
813
|
+
targetStub,
|
|
814
|
+
end
|
|
815
|
+
])
|
|
816
|
+
return routeIsClear(route, routeObstacles(edge, nodes))
|
|
817
|
+
? [{ key, lane, route }]
|
|
818
|
+
: []
|
|
819
|
+
})
|
|
820
|
+
const selected =
|
|
821
|
+
candidates.sort((leftCandidate, rightCandidate) =>
|
|
822
|
+
chartRouteLength(leftCandidate.route) + leftCandidate.lane * 8 -
|
|
823
|
+
(chartRouteLength(rightCandidate.route) + rightCandidate.lane * 8)
|
|
824
|
+
)[0]
|
|
825
|
+
if (selected === undefined || chartRouteLength(selected.route) + 16 >= chartRouteLength(points)) return null
|
|
826
|
+
lanes.set(selected.key, selected.lane + 1)
|
|
827
|
+
return selected.route
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const normalizeHierarchyRoute = (
|
|
831
|
+
edge: ChartEdge,
|
|
832
|
+
points: ReadonlyArray<ChartPoint>,
|
|
833
|
+
nodes: ReadonlyMap<string, LaidOutChartNode>,
|
|
834
|
+
allNodes: ReadonlyArray<LaidOutChartNode>,
|
|
835
|
+
lanes: Map<string, number>
|
|
836
|
+
): ReadonlyArray<ChartPoint> => {
|
|
837
|
+
if (edge.target !== null && isChartStateDescendant(edge.target, edge.source)) {
|
|
838
|
+
const source = nodes.get(edge.source)
|
|
839
|
+
const target = nodes.get(edge.target)
|
|
840
|
+
if (source === undefined) return points
|
|
841
|
+
return target === undefined
|
|
842
|
+
? anchorRouteAtCompoundHeader(points, source)
|
|
843
|
+
: localDescendantRoute(edge, points, source, target, allNodes, lanes) ??
|
|
844
|
+
anchorRouteAtCompoundHeader(points, source)
|
|
845
|
+
}
|
|
846
|
+
if (edge.target !== null && isChartStateDescendant(edge.source, edge.target)) {
|
|
847
|
+
const target = nodes.get(edge.target)
|
|
848
|
+
return target === undefined
|
|
849
|
+
? points
|
|
850
|
+
: [...anchorRouteAtCompoundHeader([...points].reverse(), target)].reverse()
|
|
851
|
+
}
|
|
852
|
+
return points
|
|
853
|
+
}
|
|
854
|
+
|
|
744
855
|
const sameSideRoute = (
|
|
745
856
|
points: ReadonlyArray<ChartPoint>,
|
|
746
857
|
side: RouteSide,
|
|
@@ -906,9 +1017,18 @@ const routeLabelCandidates = (
|
|
|
906
1017
|
points: ReadonlyArray<ChartPoint>,
|
|
907
1018
|
fallback: ChartPoint,
|
|
908
1019
|
width: number,
|
|
909
|
-
height: number
|
|
1020
|
+
height: number,
|
|
1021
|
+
nodes: ReadonlyArray<LaidOutChartNode>
|
|
910
1022
|
): ReadonlyArray<ChartPoint> => {
|
|
911
1023
|
if (isSelfTransition(edge)) return [fallback]
|
|
1024
|
+
const source = nodes.find(({ node }) => node.path === edge.source)
|
|
1025
|
+
const target = edge.target === null
|
|
1026
|
+
? undefined
|
|
1027
|
+
: nodes.find(({ node }) => node.path === edge.target)
|
|
1028
|
+
const localDescendantTarget = source?.node.type === "parallel" && target !== undefined &&
|
|
1029
|
+
target.node.parent !== source.node.path && isChartStateDescendant(target.node.path, source.node.path)
|
|
1030
|
+
? target
|
|
1031
|
+
: undefined
|
|
912
1032
|
const candidates = points.slice(1).flatMap((end, index) => {
|
|
913
1033
|
const start = points[index]!
|
|
914
1034
|
const horizontal = start.y === end.y
|
|
@@ -916,21 +1036,38 @@ const routeLabelCandidates = (
|
|
|
916
1036
|
const required = (horizontal ? width : height) + 24
|
|
917
1037
|
return length < required ? [] : [{ start, end, horizontal, length }]
|
|
918
1038
|
})
|
|
1039
|
+
const adjacent: Array<ChartPoint> = []
|
|
919
1040
|
const ordered = [
|
|
920
1041
|
...candidates.filter(({ horizontal }) => !horizontal).sort((left, right) => right.length - left.length),
|
|
921
1042
|
...candidates.filter(({ horizontal }) => horizontal).sort((left, right) => right.length - left.length)
|
|
922
1043
|
].flatMap(({ start, end, horizontal, length }) => {
|
|
923
1044
|
const clearance = (horizontal ? width : height) / 2 + 12
|
|
924
|
-
return [0.5, 2 / 3, 1 / 3].flatMap((ratio) => {
|
|
1045
|
+
return [0.5, 2 / 3, 1 / 3, 0.2, 0.8, 0.1, 0.9].flatMap((ratio) => {
|
|
925
1046
|
const distance = length * ratio
|
|
926
1047
|
if (distance < clearance || length - distance < clearance) return []
|
|
927
|
-
|
|
1048
|
+
const onRoute = {
|
|
928
1049
|
x: start.x + (end.x - start.x) * ratio,
|
|
929
1050
|
y: start.y + (end.y - start.y) * ratio
|
|
930
|
-
}
|
|
1051
|
+
}
|
|
1052
|
+
// A root container can place two routes in the same corridor. Keep the
|
|
1053
|
+
// label attached beside its own segment when no position on it is clear.
|
|
1054
|
+
for (const direction of [-1, 1]) {
|
|
1055
|
+
adjacent.push(
|
|
1056
|
+
horizontal
|
|
1057
|
+
? { x: onRoute.x, y: onRoute.y + direction * (height / 2 + chartEdgeLabelSpacing) }
|
|
1058
|
+
: { x: onRoute.x + direction * (width / 2 + chartEdgeLabelSpacing), y: onRoute.y }
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
if (horizontal || localDescendantTarget === undefined) return [onRoute]
|
|
1062
|
+
const targetCenter = localDescendantTarget.x + localDescendantTarget.width / 2
|
|
1063
|
+
const direction = targetCenter < onRoute.x ? -1 : 1
|
|
1064
|
+
return [{
|
|
1065
|
+
x: onRoute.x + direction * (width / 2 + chartEdgeLabelSpacing),
|
|
1066
|
+
y: onRoute.y
|
|
1067
|
+
}, onRoute]
|
|
931
1068
|
})
|
|
932
1069
|
})
|
|
933
|
-
return [...ordered, fallback].filter((candidate, index, all) =>
|
|
1070
|
+
return [...ordered, ...adjacent, fallback].filter((candidate, index, all) =>
|
|
934
1071
|
all.findIndex((other) => other.x === candidate.x && other.y === candidate.y) === index
|
|
935
1072
|
)
|
|
936
1073
|
}
|
|
@@ -946,7 +1083,8 @@ const placeTransitionLabels = (
|
|
|
946
1083
|
transition.points,
|
|
947
1084
|
transition.label,
|
|
948
1085
|
transition.labelWidth,
|
|
949
|
-
transition.labelHeight
|
|
1086
|
+
transition.labelHeight,
|
|
1087
|
+
nodes
|
|
950
1088
|
)
|
|
951
1089
|
const position = candidates.find((candidate) => {
|
|
952
1090
|
const candidateRect = labelRect(candidate, transition.labelWidth, transition.labelHeight)
|
|
@@ -992,7 +1130,8 @@ const placeTransitionLabels = (
|
|
|
992
1130
|
const collectLayout = (
|
|
993
1131
|
model: ChartModel,
|
|
994
1132
|
graph: ElkNode,
|
|
995
|
-
unconnected: ReadonlyArray<UnconnectedRegion
|
|
1133
|
+
unconnected: ReadonlyArray<UnconnectedRegion>,
|
|
1134
|
+
shortenRoutes = true
|
|
996
1135
|
): LaidOutChart => {
|
|
997
1136
|
const chartNodes = new Map(model.nodes.map((node) => [node.path, node]))
|
|
998
1137
|
const chartRuntimeTargets = new Map(model.runtimeTargets.map((target) => [runtimeNodeId(target), target]))
|
|
@@ -1052,9 +1191,9 @@ const collectLayout = (
|
|
|
1052
1191
|
initials.push({
|
|
1053
1192
|
initial,
|
|
1054
1193
|
x: target.x + 9,
|
|
1055
|
-
y: target.y -
|
|
1056
|
-
width:
|
|
1057
|
-
height:
|
|
1194
|
+
y: target.y - initialMarkerTopOffset,
|
|
1195
|
+
width: initialMarkerSize,
|
|
1196
|
+
height: initialMarkerSize
|
|
1058
1197
|
})
|
|
1059
1198
|
}
|
|
1060
1199
|
|
|
@@ -1074,9 +1213,9 @@ const collectLayout = (
|
|
|
1074
1213
|
chartEdge,
|
|
1075
1214
|
avoidCompoundHeaders(
|
|
1076
1215
|
chartEdge,
|
|
1077
|
-
shortenTransitionRoute(
|
|
1216
|
+
(shortenRoutes ? shortenTransitionRoute : (_edge: ChartEdge, route: ReadonlyArray<ChartPoint>) => route)(
|
|
1078
1217
|
chartEdge,
|
|
1079
|
-
normalizeHierarchyRoute(chartEdge, elkPoints, nodesByPath),
|
|
1218
|
+
normalizeHierarchyRoute(chartEdge, elkPoints, nodesByPath, nodes, hierarchyLanes),
|
|
1080
1219
|
nodesByPath,
|
|
1081
1220
|
nodes,
|
|
1082
1221
|
directLanes
|
|
@@ -1129,31 +1268,65 @@ const collectLayout = (
|
|
|
1129
1268
|
...transitionEdges,
|
|
1130
1269
|
...initialEdges
|
|
1131
1270
|
]
|
|
1132
|
-
const
|
|
1271
|
+
const contentLeft = Math.min(
|
|
1272
|
+
...nodes.map(({ x }) => x),
|
|
1273
|
+
...regions.map(({ x }) => x),
|
|
1274
|
+
...initials.map(({ x }) => x),
|
|
1275
|
+
...runtimeTargets.map(({ x }) => x),
|
|
1276
|
+
...edges.flatMap(({ points }) => points.map(({ x }) => x)),
|
|
1277
|
+
...transitionEdges.map(({ label, labelWidth }) => label.x - labelWidth / 2)
|
|
1278
|
+
)
|
|
1279
|
+
const contentTop = Math.min(
|
|
1280
|
+
...nodes.map(({ y }) => y),
|
|
1281
|
+
...regions.map(({ y }) => y),
|
|
1282
|
+
...initials.map(({ y }) => y),
|
|
1283
|
+
...runtimeTargets.map(({ y }) => y),
|
|
1284
|
+
...edges.flatMap(({ points }) => points.map(({ y }) => y)),
|
|
1285
|
+
...transitionEdges.map(({ label, labelHeight }) => label.y - labelHeight / 2)
|
|
1286
|
+
)
|
|
1287
|
+
const contentRight = Math.max(
|
|
1133
1288
|
0,
|
|
1134
1289
|
...nodes.map(({ width, x }) => x + width),
|
|
1290
|
+
...regions.map(({ width, x }) => x + width),
|
|
1135
1291
|
...initials.map(({ width, x }) => x + width),
|
|
1136
1292
|
...runtimeTargets.map(({ width, x }) => x + width),
|
|
1137
1293
|
...edges.flatMap(({ points }) => points.map(({ x }) => x)),
|
|
1138
1294
|
...transitionEdges.map(({ label, labelWidth }) => label.x + labelWidth / 2)
|
|
1139
1295
|
)
|
|
1140
|
-
const
|
|
1296
|
+
const contentBottom = Math.max(
|
|
1141
1297
|
0,
|
|
1142
1298
|
...nodes.map(({ height, y }) => y + height),
|
|
1299
|
+
...regions.map(({ height, y }) => y + height),
|
|
1143
1300
|
...initials.map(({ height, y }) => y + height),
|
|
1144
1301
|
...runtimeTargets.map(({ height, y }) => y + height),
|
|
1145
1302
|
...edges.flatMap(({ points }) => points.map(({ y }) => y)),
|
|
1146
1303
|
...transitionEdges.map(({ label, labelHeight }) => label.y + labelHeight / 2)
|
|
1147
1304
|
)
|
|
1305
|
+
const shiftX = Math.max(0, 20 - contentLeft)
|
|
1306
|
+
const shiftY = Math.max(0, 20 - contentTop)
|
|
1307
|
+
const translatePoint = ({ x, y }: ChartPoint): ChartPoint => ({ x: x + shiftX, y: y + shiftY })
|
|
1308
|
+
const translatedTransitionEdges = transitionEdges.map((edge): LaidOutChartTransition => ({
|
|
1309
|
+
...edge,
|
|
1310
|
+
points: edge.points.map(translatePoint),
|
|
1311
|
+
label: translatePoint(edge.label)
|
|
1312
|
+
}))
|
|
1313
|
+
const translatedInitialEdges = initialEdges.map((edge): LaidOutChartInitialEdge => ({
|
|
1314
|
+
...edge,
|
|
1315
|
+
points: edge.points.map(translatePoint)
|
|
1316
|
+
}))
|
|
1148
1317
|
|
|
1149
1318
|
return {
|
|
1150
|
-
width: Math.max(360, graph.width ?? 0,
|
|
1151
|
-
height: Math.max(280, graph.height ?? 0,
|
|
1152
|
-
regions,
|
|
1153
|
-
nodes,
|
|
1154
|
-
initials,
|
|
1155
|
-
runtimeTargets
|
|
1156
|
-
|
|
1319
|
+
width: Math.max(360, (graph.width ?? 0) + shiftX, contentRight + shiftX + 20),
|
|
1320
|
+
height: Math.max(280, (graph.height ?? 0) + shiftY, contentBottom + shiftY + 20),
|
|
1321
|
+
regions: regions.map((region) => ({ ...region, x: region.x + shiftX, y: region.y + shiftY })),
|
|
1322
|
+
nodes: nodes.map((node) => ({ ...node, x: node.x + shiftX, y: node.y + shiftY })),
|
|
1323
|
+
initials: initials.map((initial) => ({ ...initial, x: initial.x + shiftX, y: initial.y + shiftY })),
|
|
1324
|
+
runtimeTargets: runtimeTargets.map((target) => ({
|
|
1325
|
+
...target,
|
|
1326
|
+
x: target.x + shiftX,
|
|
1327
|
+
y: target.y + shiftY
|
|
1328
|
+
})),
|
|
1329
|
+
edges: [...translatedTransitionEdges, ...translatedInitialEdges]
|
|
1157
1330
|
}
|
|
1158
1331
|
}
|
|
1159
1332
|
|
|
@@ -1299,8 +1472,8 @@ const collinearOverlap = (left: OrthogonalSegment, right: OrthogonalSegment): nu
|
|
|
1299
1472
|
|
|
1300
1473
|
const transitionTouchesNode = (edge: ChartEdge, path: string): boolean => {
|
|
1301
1474
|
const target = edgeTargetPath(edge)
|
|
1302
|
-
return edge.source === path ||
|
|
1303
|
-
target === path || target !== null &&
|
|
1475
|
+
return edge.source === path || isChartStateDescendant(edge.source, path) ||
|
|
1476
|
+
target === path || target !== null && isChartStateDescendant(target, path)
|
|
1304
1477
|
}
|
|
1305
1478
|
|
|
1306
1479
|
const laidOutEdgeId = (edge: LaidOutChartTransition | LaidOutChartInitialEdge): string =>
|
|
@@ -1348,7 +1521,7 @@ export const validateChartLayout = (
|
|
|
1348
1521
|
}
|
|
1349
1522
|
if (nodeBoundaryDistance(start, source) > 0.5) {
|
|
1350
1523
|
report("detached-source", transition.edge.id, transition.edge.source)
|
|
1351
|
-
} else if (!isOutwardStep(start, next,
|
|
1524
|
+
} else if (!isOutwardStep(start, next, endpointStepSide(start, next, source))) {
|
|
1352
1525
|
report("wrong-source-direction", transition.edge.id, transition.edge.source)
|
|
1353
1526
|
}
|
|
1354
1527
|
}
|
|
@@ -1364,7 +1537,10 @@ export const validateChartLayout = (
|
|
|
1364
1537
|
const target = layout.nodes.find(({ node }) => node.path === transition.edge.target)
|
|
1365
1538
|
if (target !== undefined && nodeBoundaryDistance(end, target) > 0.5) {
|
|
1366
1539
|
report("detached-terminal", transition.edge.id, transition.edge.target)
|
|
1367
|
-
} else if (
|
|
1540
|
+
} else if (
|
|
1541
|
+
target !== undefined && bend !== undefined &&
|
|
1542
|
+
!isOutwardStep(end, bend, endpointStepSide(end, bend, target))
|
|
1543
|
+
) {
|
|
1368
1544
|
report("wrong-terminal-direction", transition.edge.id, transition.edge.target)
|
|
1369
1545
|
}
|
|
1370
1546
|
}
|
|
@@ -1417,7 +1593,7 @@ export const validateChartLayout = (
|
|
|
1417
1593
|
for (const initial of initialEdges) {
|
|
1418
1594
|
for (const node of layout.nodes) {
|
|
1419
1595
|
const containsTarget = node.node.path === initial.initial.target ||
|
|
1420
|
-
|
|
1596
|
+
isChartStateDescendant(initial.initial.target, node.node.path)
|
|
1421
1597
|
const obstacle = node.node.children.length > 0 && containsTarget
|
|
1422
1598
|
? nodeHeaderRect(node)
|
|
1423
1599
|
: nodeRect(node)
|
|
@@ -1446,11 +1622,14 @@ export const validateChartLayout = (
|
|
|
1446
1622
|
}
|
|
1447
1623
|
|
|
1448
1624
|
const allSegments = segments(layout)
|
|
1625
|
+
const transitionIds = new Map(model.edges.map((edge) => [edge.id, edge.transitionId]))
|
|
1449
1626
|
for (let left = 0; left < allSegments.length; left++) {
|
|
1450
1627
|
for (let right = left + 1; right < allSegments.length; right++) {
|
|
1451
1628
|
const first = allSegments[left]!
|
|
1452
1629
|
const second = allSegments[right]!
|
|
1453
1630
|
if (first.edgeId === second.edgeId) continue
|
|
1631
|
+
const firstTransition = transitionIds.get(first.edgeId)
|
|
1632
|
+
if (firstTransition !== undefined && firstTransition === transitionIds.get(second.edgeId)) continue
|
|
1454
1633
|
if (collinearOverlap(first, second) > 4) report("route-overlap", first.edgeId, second.edgeId)
|
|
1455
1634
|
}
|
|
1456
1635
|
}
|
|
@@ -1540,6 +1719,12 @@ export const layoutChartWith = (
|
|
|
1540
1719
|
const validation = validate(model, candidate)
|
|
1541
1720
|
if (validation.valid) return Effect.succeed(candidate)
|
|
1542
1721
|
invalid.push({ profile: profile.id, layout: candidate, validation })
|
|
1722
|
+
// Route shortening can crowd labels beside unrelated transitions.
|
|
1723
|
+
// Keep the original ELK corridor as a checked alternative.
|
|
1724
|
+
const originalRoutes = collectLayout(model, graph, regions, false)
|
|
1725
|
+
const originalValidation = validate(model, originalRoutes)
|
|
1726
|
+
if (originalValidation.valid) return Effect.succeed(originalRoutes)
|
|
1727
|
+
invalid.push({ profile: profile.id, layout: originalRoutes, validation: originalValidation })
|
|
1543
1728
|
return attempt(index + 1)
|
|
1544
1729
|
}
|
|
1545
1730
|
}
|
|
@@ -71,6 +71,9 @@ export interface ChartModel {
|
|
|
71
71
|
readonly initials: ReadonlyArray<ChartInitial>
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export const isChartStateDescendant = (path: string, ancestor: string): boolean =>
|
|
75
|
+
ancestor === "" ? path !== "" : path.startsWith(`${ancestor}.`)
|
|
76
|
+
|
|
74
77
|
const activityLabel = (activity: VisualizationActivity): string => {
|
|
75
78
|
switch (activity.type) {
|
|
76
79
|
case "machine":
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
layoutChart,
|
|
9
9
|
maxVisibleActivities
|
|
10
10
|
} from "./chart-layout.js"
|
|
11
|
-
import { type ChartEdgeBadge, makeChartModel } from "./chart-model.js"
|
|
11
|
+
import { type ChartEdge, type ChartEdgeBadge, isChartStateDescendant, makeChartModel } from "./chart-model.js"
|
|
12
12
|
|
|
13
13
|
export interface ChartHandlers {
|
|
14
14
|
readonly selectState: (path: string, anchor: ChartInteractionAnchor) => void
|
|
@@ -60,6 +60,9 @@ export const minimumChartZoom = 0.2
|
|
|
60
60
|
export const maximumChartZoom = 1.6
|
|
61
61
|
export const chartPanThreshold = 5
|
|
62
62
|
|
|
63
|
+
export const isParentChildTransition = (edge: ChartEdge): boolean =>
|
|
64
|
+
edge.target !== null && isChartStateDescendant(edge.target, edge.source)
|
|
65
|
+
|
|
63
66
|
const clampZoom = (zoom: number): number => Math.min(maximumChartZoom, Math.max(minimumChartZoom, zoom))
|
|
64
67
|
|
|
65
68
|
export const chartZoomScrollPosition = (
|
|
@@ -384,7 +387,6 @@ const render = (
|
|
|
384
387
|
const transitionElements = new Map<string, Array<Element>>()
|
|
385
388
|
const edgeElements = new Map<string, Array<Element>>()
|
|
386
389
|
const transitionControls = new Map<string, Array<HTMLButtonElement>>()
|
|
387
|
-
const parentByState = new Map(layout.nodes.map(({ node }) => [node.path, node.parent]))
|
|
388
390
|
const chartEdges = new Map(
|
|
389
391
|
layout.edges.flatMap((laidOut) => laidOut.kind === "transition" ? [[laidOut.edge.id, laidOut.edge] as const] : [])
|
|
390
392
|
)
|
|
@@ -474,8 +476,7 @@ const render = (
|
|
|
474
476
|
}
|
|
475
477
|
|
|
476
478
|
for (const laidOut of layout.edges) {
|
|
477
|
-
const parentChild = laidOut.kind === "transition" && laidOut.edge
|
|
478
|
-
parentByState.get(laidOut.edge.target) === laidOut.edge.source
|
|
479
|
+
const parentChild = laidOut.kind === "transition" && isParentChildTransition(laidOut.edge)
|
|
479
480
|
const failure = laidOut.kind === "transition" &&
|
|
480
481
|
laidOut.edge.badges.some((badge) => badge.type === "failure")
|
|
481
482
|
const group = svgElement(
|