@robotaccomplice/architext 1.0.0 → 1.1.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/README.md +14 -4
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/docs/architecture/ROUTING_FRAMEWORK_COMPARISON.md +284 -0
- package/docs/architecture/ROUTING_PLAN.md +480 -0
- package/docs/architext/dist/assets/index-DMbdxljw.js +51 -0
- package/docs/architext/dist/assets/index-DvokFPhn.css +1 -0
- package/docs/architext/dist/assets/planningWorker-DY_nEecj.js +1 -0
- package/docs/architext/dist/index.html +2 -2
- package/docs/architext/src/main.tsx +450 -556
- package/docs/architext/src/routing/planDiagram.js +208 -0
- package/docs/architext/src/routing/planningWorker.js +15 -0
- package/docs/architext/src/routing/routeEdges.js +1484 -0
- package/docs/architext/src/styles.css +180 -1
- package/docs/architext/tsconfig.json +1 -2
- package/docs/assets/screenshots/architext-c4.png +0 -0
- package/docs/assets/screenshots/architext-data-risks.png +0 -0
- package/docs/assets/screenshots/architext-flows.png +0 -0
- package/docs/assets/screenshots/architext-sequence.png +0 -0
- package/package.json +9 -4
- package/docs/architext/dist/assets/index-BWZ6sEpA.js +0 -51
- package/docs/architext/dist/assets/index-iWLms0Pa.css +0 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { routeEdges } from "./routeEdges.js";
|
|
2
|
+
|
|
3
|
+
function estimatedLabelBox(route, relationship) {
|
|
4
|
+
return labelBoxAt(route.labelX, route.labelY, relationship);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function labelBoxAt(x, y, relationship) {
|
|
8
|
+
const text = relationship.label ?? relationship.id ?? "";
|
|
9
|
+
const width = Math.max(24, Math.min(180, text.length * 6 + 12));
|
|
10
|
+
const height = relationship.relationshipType === "flow" || relationship.stepId ? 24 : 18;
|
|
11
|
+
return {
|
|
12
|
+
x: x - width / 2,
|
|
13
|
+
y: y - height / 2,
|
|
14
|
+
width,
|
|
15
|
+
height
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rectsOverlap(a, b, padding = 0) {
|
|
20
|
+
return (
|
|
21
|
+
a.x < b.x + b.width + padding &&
|
|
22
|
+
a.x + a.width > b.x - padding &&
|
|
23
|
+
a.y < b.y + b.height + padding &&
|
|
24
|
+
a.y + a.height > b.y - padding
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function labelPlacementCandidates(route) {
|
|
29
|
+
const offsets = [];
|
|
30
|
+
for (const y of [0, -24, 24, -48, 48, -72, 72, -96, 96, -120, 120, -144, 144, -168, 168, -192, 192]) {
|
|
31
|
+
for (const x of [0, 36, -36, 64, -64, 96, -96, 128, -128]) {
|
|
32
|
+
offsets.push([x, y]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return offsets
|
|
36
|
+
.sort((a, b) => Math.hypot(a[0], a[1]) - Math.hypot(b[0], b[1]))
|
|
37
|
+
.map(([x, y]) => ({ x: route.labelX + x, y: route.labelY + y }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function placeLabel(route, relationship, nodeRects, placedLabels, canvasWidth, canvasHeight) {
|
|
41
|
+
const candidates = labelPlacementCandidates(route);
|
|
42
|
+
const scored = candidates.map((candidate, index) => {
|
|
43
|
+
const box = labelBoxAt(candidate.x, candidate.y, relationship);
|
|
44
|
+
const qualityCosts = {
|
|
45
|
+
labelMovementCost: Math.hypot(candidate.x - route.labelX, candidate.y - route.labelY),
|
|
46
|
+
labelSearchOrderCost: index * 4,
|
|
47
|
+
labelBoundaryCost: 0,
|
|
48
|
+
labelNodeConflictCost: 0,
|
|
49
|
+
labelConflictCost: 0
|
|
50
|
+
};
|
|
51
|
+
if (box.x < 8 || box.y < 8 || box.x + box.width > canvasWidth - 8 || box.y + box.height > canvasHeight - 8) {
|
|
52
|
+
qualityCosts.labelBoundaryCost += 100000;
|
|
53
|
+
}
|
|
54
|
+
for (const [nodeId, rect] of nodeRects) {
|
|
55
|
+
if (nodeId === relationship.from || nodeId === relationship.to) continue;
|
|
56
|
+
if (rectsOverlap(box, rect, 4)) qualityCosts.labelNodeConflictCost += 80000;
|
|
57
|
+
}
|
|
58
|
+
for (const placed of placedLabels) {
|
|
59
|
+
if (rectsOverlap(box, placed, 2)) qualityCosts.labelConflictCost += 20000;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
candidate,
|
|
63
|
+
box,
|
|
64
|
+
cost: Object.values(qualityCosts).reduce((sum, value) => sum + value, 0),
|
|
65
|
+
qualityCosts
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return scored.sort((a, b) => a.cost - b.cost)[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function planDiagram(input) {
|
|
72
|
+
const nodeWidth = input.nodeWidth;
|
|
73
|
+
const nodeHeight = input.nodeHeight;
|
|
74
|
+
const laneWidth = input.laneWidth;
|
|
75
|
+
const rowGap = input.rowGap;
|
|
76
|
+
const marginX = input.marginX;
|
|
77
|
+
const marginY = input.marginY;
|
|
78
|
+
const visibleNodeIds = new Set(input.visibleNodeIds);
|
|
79
|
+
const laneIndexByNode = new Map();
|
|
80
|
+
const rowIndexByNode = new Map();
|
|
81
|
+
|
|
82
|
+
input.view.lanes.forEach((lane, laneIndex) => {
|
|
83
|
+
lane.nodeIds.forEach((nodeId, rowIndex) => {
|
|
84
|
+
if (!visibleNodeIds.has(nodeId)) return;
|
|
85
|
+
laneIndexByNode.set(nodeId, laneIndex);
|
|
86
|
+
rowIndexByNode.set(nodeId, rowIndex);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const maxRows = Math.max(...input.view.lanes.map((lane) => lane.nodeIds.filter((nodeId) => visibleNodeIds.has(nodeId)).length), 1);
|
|
91
|
+
const canvasWidth = Math.max(input.minCanvasWidth, marginX * 2 + input.view.lanes.length * laneWidth + input.canvasExtraWidth);
|
|
92
|
+
const canvasHeight = Math.max(input.minCanvasHeight, marginY + maxRows * rowGap + input.canvasExtraHeight);
|
|
93
|
+
const positionFor = (nodeId) => ({
|
|
94
|
+
x: marginX + (laneIndexByNode.get(nodeId) ?? 0) * laneWidth,
|
|
95
|
+
y: marginY + (rowIndexByNode.get(nodeId) ?? 0) * rowGap
|
|
96
|
+
});
|
|
97
|
+
const nodeRects = new Map(Array.from(visibleNodeIds).map((nodeId) => {
|
|
98
|
+
const position = positionFor(nodeId);
|
|
99
|
+
return [
|
|
100
|
+
nodeId,
|
|
101
|
+
{
|
|
102
|
+
x: position.x,
|
|
103
|
+
y: position.y,
|
|
104
|
+
width: nodeWidth,
|
|
105
|
+
height: nodeHeight
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
}));
|
|
109
|
+
const routes = routeEdges({
|
|
110
|
+
relationships: input.relationships,
|
|
111
|
+
visibleNodeIds,
|
|
112
|
+
nodeRects,
|
|
113
|
+
laneIndexByNode,
|
|
114
|
+
rowIndexByNode,
|
|
115
|
+
canvasWidth,
|
|
116
|
+
canvasHeight,
|
|
117
|
+
marginY,
|
|
118
|
+
style: input.style,
|
|
119
|
+
stats: input.stats
|
|
120
|
+
});
|
|
121
|
+
const relationshipsById = new Map(input.relationships.map((relationship) => [relationship.id, relationship]));
|
|
122
|
+
const plannedRoutes = new Map();
|
|
123
|
+
const labelBoxes = new Map();
|
|
124
|
+
const placedLabels = [];
|
|
125
|
+
|
|
126
|
+
for (const [relationshipId, route] of routes) {
|
|
127
|
+
const relationship = relationshipsById.get(relationshipId);
|
|
128
|
+
if (relationship) {
|
|
129
|
+
const labelPlacement = placeLabel(route, relationship, nodeRects, placedLabels, canvasWidth, canvasHeight);
|
|
130
|
+
const labelQualityCosts = {
|
|
131
|
+
...route.qualityCosts,
|
|
132
|
+
labelMovementCost: 0,
|
|
133
|
+
labelSearchOrderCost: 0,
|
|
134
|
+
labelBoundaryCost: 0,
|
|
135
|
+
labelNodeConflictCost: 0,
|
|
136
|
+
labelConflictCost: 0,
|
|
137
|
+
...labelPlacement.qualityCosts
|
|
138
|
+
};
|
|
139
|
+
const plannedRoute = {
|
|
140
|
+
...route,
|
|
141
|
+
labelX: labelPlacement.candidate.x,
|
|
142
|
+
labelY: labelPlacement.candidate.y,
|
|
143
|
+
qualityCosts: labelQualityCosts,
|
|
144
|
+
cost: Object.values(labelQualityCosts).reduce((sum, value) => sum + value, 0)
|
|
145
|
+
};
|
|
146
|
+
plannedRoutes.set(relationshipId, plannedRoute);
|
|
147
|
+
labelBoxes.set(relationshipId, labelPlacement.box);
|
|
148
|
+
placedLabels.push(labelPlacement.box);
|
|
149
|
+
} else {
|
|
150
|
+
plannedRoutes.set(relationshipId, route);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const warnings = [];
|
|
155
|
+
for (const [relationshipId, route] of plannedRoutes) {
|
|
156
|
+
for (const warning of route.warnings ?? []) {
|
|
157
|
+
warnings.push({ ...warning, relationshipId });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [relationshipId, labelBox] of labelBoxes) {
|
|
161
|
+
const relationship = relationshipsById.get(relationshipId);
|
|
162
|
+
for (const [nodeId, rect] of nodeRects) {
|
|
163
|
+
if (nodeId === relationship?.from || nodeId === relationship?.to) continue;
|
|
164
|
+
if (rectsOverlap(labelBox, rect, 4)) {
|
|
165
|
+
warnings.push({
|
|
166
|
+
code: "label-over-node",
|
|
167
|
+
message: "Route label overlaps a non-endpoint node.",
|
|
168
|
+
relationshipId,
|
|
169
|
+
nodeId
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const labelEntries = [...labelBoxes];
|
|
175
|
+
for (let index = 0; index < labelEntries.length; index += 1) {
|
|
176
|
+
const [relationshipId, labelBox] = labelEntries[index];
|
|
177
|
+
for (let otherIndex = index + 1; otherIndex < labelEntries.length; otherIndex += 1) {
|
|
178
|
+
const [otherRelationshipId, otherLabelBox] = labelEntries[otherIndex];
|
|
179
|
+
if (rectsOverlap(labelBox, otherLabelBox, 2)) {
|
|
180
|
+
warnings.push({
|
|
181
|
+
code: "label-over-label",
|
|
182
|
+
message: "Route label overlaps another route label.",
|
|
183
|
+
relationshipId,
|
|
184
|
+
otherRelationshipId
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
canvasWidth,
|
|
192
|
+
canvasHeight,
|
|
193
|
+
nodeWidth,
|
|
194
|
+
nodeHeight,
|
|
195
|
+
laneWidth,
|
|
196
|
+
rowGap,
|
|
197
|
+
marginX,
|
|
198
|
+
marginY,
|
|
199
|
+
visibleNodeIds,
|
|
200
|
+
laneIndexByNode,
|
|
201
|
+
rowIndexByNode,
|
|
202
|
+
nodeRects,
|
|
203
|
+
routes: plannedRoutes,
|
|
204
|
+
labelBoxes,
|
|
205
|
+
warnings,
|
|
206
|
+
positionFor
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { planDiagram } from "./planDiagram.js";
|
|
2
|
+
|
|
3
|
+
self.onmessage = (event) => {
|
|
4
|
+
const { key, input } = event.data;
|
|
5
|
+
try {
|
|
6
|
+
const plan = planDiagram(input);
|
|
7
|
+
const { positionFor, ...cloneablePlan } = plan;
|
|
8
|
+
self.postMessage({ key, plan: cloneablePlan });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
self.postMessage({
|
|
11
|
+
key,
|
|
12
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|