trace-viz 0.3.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/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/adapters/index.d.ts +32 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/agent.d.ts +66 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +329 -0
- package/dist/agent.js.map +1 -0
- package/dist/atlas/atlas.d.ts +203 -0
- package/dist/atlas/atlas.d.ts.map +1 -0
- package/dist/atlas/atlas.js +1466 -0
- package/dist/atlas/atlas.js.map +1 -0
- package/dist/atlas/index.d.ts +7 -0
- package/dist/atlas/index.d.ts.map +1 -0
- package/dist/atlas/index.js +4 -0
- package/dist/atlas/index.js.map +1 -0
- package/dist/atlas/layout.d.ts +44 -0
- package/dist/atlas/layout.d.ts.map +1 -0
- package/dist/atlas/layout.js +103 -0
- package/dist/atlas/layout.js.map +1 -0
- package/dist/atlas/model.d.ts +164 -0
- package/dist/atlas/model.d.ts.map +1 -0
- package/dist/atlas/model.js +489 -0
- package/dist/atlas/model.js.map +1 -0
- package/dist/channels.d.ts +105 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +411 -0
- package/dist/channels.js.map +1 -0
- package/dist/components.d.ts +76 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +432 -0
- package/dist/components.js.map +1 -0
- package/dist/estate.d.ts +25 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +285 -0
- package/dist/estate.js.map +1 -0
- package/dist/failure.d.ts +36 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +82 -0
- package/dist/failure.js.map +1 -0
- package/dist/geometry.d.ts +24 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +92 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +66 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +152 -0
- package/dist/layout.js.map +1 -0
- package/dist/layout3d.d.ts +98 -0
- package/dist/layout3d.d.ts.map +1 -0
- package/dist/layout3d.js +350 -0
- package/dist/layout3d.js.map +1 -0
- package/dist/mesh.d.ts +23 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +263 -0
- package/dist/mesh.js.map +1 -0
- package/dist/model.d.ts +94 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +84 -0
- package/dist/model.js.map +1 -0
- package/dist/particles.d.ts +80 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +118 -0
- package/dist/particles.js.map +1 -0
- package/dist/project.d.ts +113 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +288 -0
- package/dist/project.js.map +1 -0
- package/dist/react/index.d.ts +22 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +69 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer.d.ts +252 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +1710 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scales.d.ts +99 -0
- package/dist/scales.d.ts.map +1 -0
- package/dist/scales.js +126 -0
- package/dist/scales.js.map +1 -0
- package/dist/scene.d.ts +53 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +44 -0
- package/dist/scene.js.map +1 -0
- package/dist/schema.d.ts +24 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +131 -0
- package/dist/schema.js.map +1 -0
- package/dist/seed.d.ts +33 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +173 -0
- package/dist/seed.js.map +1 -0
- package/dist/theme.d.ts +35 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +83 -0
- package/dist/theme.js.map +1 -0
- package/dist/trace-viz.esm.js +15 -0
- package/dist/trace-viz.min.js +15 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +105 -0
package/dist/layout3d.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { defaultTier } from './components.js';
|
|
2
|
+
import { mulberry32 } from './particles.js';
|
|
3
|
+
export function project3(p, cam) {
|
|
4
|
+
const cos = Math.cos(cam.yaw);
|
|
5
|
+
const sin = Math.sin(cam.yaw);
|
|
6
|
+
const rx = p.x * cos - p.z * sin;
|
|
7
|
+
const depth = p.x * sin + p.z * cos;
|
|
8
|
+
return {
|
|
9
|
+
x: cam.cx + rx * cam.scale,
|
|
10
|
+
y: cam.cy - p.y * cam.scaleY + depth * cam.scaleY * cam.tilt,
|
|
11
|
+
depth,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export const defaultWorldLayout = {
|
|
15
|
+
tierOf: defaultTier,
|
|
16
|
+
attractionOf: () => 0.5,
|
|
17
|
+
tierGap: 1.4,
|
|
18
|
+
nodeWidth: 62,
|
|
19
|
+
nodeHeight: 52,
|
|
20
|
+
iterations: 320,
|
|
21
|
+
seed: 7,
|
|
22
|
+
tilt: 0.3,
|
|
23
|
+
gap: 1.2,
|
|
24
|
+
labelSpace: 26,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Place nodes in the world.
|
|
28
|
+
*
|
|
29
|
+
* A short deterministic relaxation, not a live force simulation: same scene in,
|
|
30
|
+
* same world out, so two screenshots are comparable and a re-render does not
|
|
31
|
+
* shuffle everything the user had just learned to read.
|
|
32
|
+
*/
|
|
33
|
+
export function layoutWorld(scene, options = {}) {
|
|
34
|
+
const opt = { ...defaultWorldLayout, ...options };
|
|
35
|
+
const rand = mulberry32(opt.seed);
|
|
36
|
+
const nodes = scene.nodes;
|
|
37
|
+
if (!nodes.length)
|
|
38
|
+
return new Map();
|
|
39
|
+
const tiers = nodes.map((n) => opt.tierOf(n));
|
|
40
|
+
const maxTier = Math.max(...tiers, 0);
|
|
41
|
+
// Seed each plane on a ring, which starts things spread out and keeps the
|
|
42
|
+
// relaxation from having to undo a pile in the middle.
|
|
43
|
+
const perTier = new Map();
|
|
44
|
+
for (const t of tiers)
|
|
45
|
+
perTier.set(t, (perTier.get(t) ?? 0) + 1);
|
|
46
|
+
const seenInTier = new Map();
|
|
47
|
+
const px = [];
|
|
48
|
+
const pz = [];
|
|
49
|
+
for (const [i, node] of nodes.entries()) {
|
|
50
|
+
const tier = tiers[i];
|
|
51
|
+
const idx = seenInTier.get(tier) ?? 0;
|
|
52
|
+
seenInTier.set(tier, idx + 1);
|
|
53
|
+
const count = perTier.get(tier) ?? 1;
|
|
54
|
+
const angle = (idx / count) * Math.PI * 2 + tier * 0.7;
|
|
55
|
+
const radius = 0.55 + (rand() - 0.5) * 0.12;
|
|
56
|
+
px.push(Math.cos(angle) * radius);
|
|
57
|
+
pz.push(Math.sin(angle) * radius);
|
|
58
|
+
void node;
|
|
59
|
+
}
|
|
60
|
+
const index = new Map(nodes.map((n, i) => [n.id, i]));
|
|
61
|
+
const links = [];
|
|
62
|
+
for (const e of scene.edges) {
|
|
63
|
+
const a = index.get(e.from);
|
|
64
|
+
const b = index.get(e.to);
|
|
65
|
+
if (a === undefined || b === undefined || a === b)
|
|
66
|
+
continue;
|
|
67
|
+
links.push({ a, b, w: opt.attractionOf(e.id) });
|
|
68
|
+
}
|
|
69
|
+
// Categorical grouping is a second kind of link: same value, fixed pull.
|
|
70
|
+
if (opt.groupOf) {
|
|
71
|
+
const buckets = new Map();
|
|
72
|
+
for (const [i, node] of nodes.entries()) {
|
|
73
|
+
const g = opt.groupOf(node);
|
|
74
|
+
if (!g)
|
|
75
|
+
continue;
|
|
76
|
+
(buckets.get(g) ?? buckets.set(g, []).get(g)).push(i);
|
|
77
|
+
}
|
|
78
|
+
for (const members of buckets.values()) {
|
|
79
|
+
for (let i = 0; i < members.length; i++) {
|
|
80
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
81
|
+
links.push({ a: members[i], b: members[j], w: 0.55 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const n = nodes.length;
|
|
87
|
+
const fx = new Float64Array(n);
|
|
88
|
+
const fz = new Float64Array(n);
|
|
89
|
+
for (let step = 0; step < opt.iterations; step++) {
|
|
90
|
+
const cool = 1 - step / opt.iterations;
|
|
91
|
+
fx.fill(0);
|
|
92
|
+
fz.fill(0);
|
|
93
|
+
// Repulsion. Cross-tier pairs still repel, because they overlap on screen
|
|
94
|
+
// even though they never touch in world space.
|
|
95
|
+
for (let i = 0; i < n; i++) {
|
|
96
|
+
for (let j = i + 1; j < n; j++) {
|
|
97
|
+
let dx = px[i] - px[j];
|
|
98
|
+
let dz = pz[i] - pz[j];
|
|
99
|
+
let d2 = dx * dx + dz * dz;
|
|
100
|
+
if (d2 < 1e-6) {
|
|
101
|
+
dx = (rand() - 0.5) * 0.01;
|
|
102
|
+
dz = (rand() - 0.5) * 0.01;
|
|
103
|
+
d2 = 1e-4;
|
|
104
|
+
}
|
|
105
|
+
const sameTier = tiers[i] === tiers[j];
|
|
106
|
+
const strength = (sameTier ? 0.02 : 0.006) / d2;
|
|
107
|
+
const d = Math.sqrt(d2);
|
|
108
|
+
fx[i] += (dx / d) * strength;
|
|
109
|
+
fz[i] += (dz / d) * strength;
|
|
110
|
+
fx[j] -= (dx / d) * strength;
|
|
111
|
+
fz[j] -= (dz / d) * strength;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Attraction along links, weighted by whatever is bound to distance.
|
|
115
|
+
for (const link of links) {
|
|
116
|
+
const dx = px[link.b] - px[link.a];
|
|
117
|
+
const dz = pz[link.b] - pz[link.a];
|
|
118
|
+
const pull = 0.02 * (0.15 + link.w);
|
|
119
|
+
fx[link.a] += dx * pull;
|
|
120
|
+
fz[link.a] += dz * pull;
|
|
121
|
+
fx[link.b] -= dx * pull;
|
|
122
|
+
fz[link.b] -= dz * pull;
|
|
123
|
+
}
|
|
124
|
+
// Weak gravity, so disconnected nodes do not drift to infinity.
|
|
125
|
+
for (let i = 0; i < n; i++) {
|
|
126
|
+
fx[i] -= px[i] * 0.006;
|
|
127
|
+
fz[i] -= pz[i] * 0.006;
|
|
128
|
+
px[i] += fx[i] * cool * 2.2;
|
|
129
|
+
pz[i] += fz[i] * cool * 2.2;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Flatten the planes before spacing them out. A square plane spends the
|
|
133
|
+
// canvas's vertical budget on depth, and vertical budget is exactly what the
|
|
134
|
+
// tiers need: services have to sit visibly *above* the data they read, and
|
|
135
|
+
// deep planes bleed into each other until that reading is gone.
|
|
136
|
+
for (let i = 0; i < n; i++)
|
|
137
|
+
pz[i] *= 0.42;
|
|
138
|
+
// Force relaxation gets the *arrangement* right - who is near whom - but on
|
|
139
|
+
// its own it happily stacks two boxes on the same spot, and an overlapping
|
|
140
|
+
// pile is worse than a wrong arrangement. So the neighbourhood comes from the
|
|
141
|
+
// forces and the spacing is then enforced outright.
|
|
142
|
+
separate(px, pz, tiers, rand, opt.tilt, opt.gap);
|
|
143
|
+
// No normalisation. Rescaling to a unit box here would undo the spacing that
|
|
144
|
+
// was just enforced - the more crowded the plane, the harder it would squeeze.
|
|
145
|
+
// The camera does the fitting instead, in pixels, where it belongs.
|
|
146
|
+
const centreX = px.reduce((a, b) => a + b, 0) / n;
|
|
147
|
+
const centreZ = pz.reduce((a, b) => a + b, 0) / n;
|
|
148
|
+
/**
|
|
149
|
+
* How far apart the planes have to be so they do not overlap on screen.
|
|
150
|
+
*
|
|
151
|
+
* A plane is not a line: its own depth projects to `2 * zExtent * tilt` of
|
|
152
|
+
* vertical space. Any tier gap smaller than that guarantees the services
|
|
153
|
+
* plane bleeds into the data plane, which destroys the one reading the world
|
|
154
|
+
* exists to give - what runs above what. So the gap is derived from the
|
|
155
|
+
* depth actually used, not configured and hoped for.
|
|
156
|
+
*/
|
|
157
|
+
const planeGap = () => {
|
|
158
|
+
let zExtent = 0.2;
|
|
159
|
+
for (let i = 0; i < n; i++)
|
|
160
|
+
zExtent = Math.max(zExtent, Math.abs(pz[i]));
|
|
161
|
+
return Math.max(opt.tierGap, 2 * zExtent * opt.tilt + 0.62);
|
|
162
|
+
};
|
|
163
|
+
const build = () => {
|
|
164
|
+
const gap = planeGap();
|
|
165
|
+
const out = new Map();
|
|
166
|
+
for (const [i, node] of nodes.entries()) {
|
|
167
|
+
const size = opt.sizeOf?.(node) ?? { w: opt.nodeWidth, h: opt.nodeHeight };
|
|
168
|
+
out.set(node.id, {
|
|
169
|
+
id: node.id,
|
|
170
|
+
x: px[i] - centreX,
|
|
171
|
+
z: pz[i] - centreZ,
|
|
172
|
+
y: (maxTier - tiers[i]) * gap,
|
|
173
|
+
tier: tiers[i],
|
|
174
|
+
w: size.w,
|
|
175
|
+
h: size.h,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
};
|
|
180
|
+
// Final pass, and the one that actually decides whether the world is legible:
|
|
181
|
+
// separate the bodies in *pixels*, against the camera that will draw them.
|
|
182
|
+
// World-space spacing cannot do this on its own, because the camera rescales
|
|
183
|
+
// to fit - push things further apart in the world and the camera simply zooms
|
|
184
|
+
// out, leaving the overlap exactly where it was.
|
|
185
|
+
if (opt.viewport) {
|
|
186
|
+
for (let round = 0; round < 5; round++) {
|
|
187
|
+
const current = build();
|
|
188
|
+
const cam = fitCamera(current, opt.viewport.width, opt.viewport.height, opt.viewport.yaw, opt.tilt, undefined, worldExtent(current));
|
|
189
|
+
if (!separateOnScreen(px, pz, tiers, nodes, current, cam, opt))
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return build();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resolve overlapping bodies in screen space, then convert the push back into
|
|
197
|
+
* world coordinates. Returns whether anything moved.
|
|
198
|
+
*/
|
|
199
|
+
function separateOnScreen(px, pz, tiers, nodes, world, cam, opt) {
|
|
200
|
+
const n = nodes.length;
|
|
201
|
+
const boxes = nodes.map((node, i) => {
|
|
202
|
+
const w = world.get(node.id);
|
|
203
|
+
const p = project3(w, cam);
|
|
204
|
+
return { i, x: p.x, y: p.y, w: w.w, h: w.h + opt.labelSpace, tier: tiers[i] };
|
|
205
|
+
});
|
|
206
|
+
const cos = Math.cos(cam.yaw);
|
|
207
|
+
const sin = Math.sin(cam.yaw);
|
|
208
|
+
const unproject = (dxScreen, dyScreen) => {
|
|
209
|
+
const a = dxScreen / cam.scale;
|
|
210
|
+
const b = dyScreen / (cam.scaleY * cam.tilt);
|
|
211
|
+
return { dx: a * cos + b * sin, dz: -a * sin + b * cos };
|
|
212
|
+
};
|
|
213
|
+
let moved = false;
|
|
214
|
+
for (let pass = 0; pass < 24; pass++) {
|
|
215
|
+
let touched = false;
|
|
216
|
+
for (let i = 0; i < n; i++) {
|
|
217
|
+
for (let j = i + 1; j < n; j++) {
|
|
218
|
+
const a = boxes[i];
|
|
219
|
+
const b = boxes[j];
|
|
220
|
+
// Different planes are already separated vertically by the tier gap;
|
|
221
|
+
// only their in-plane positions need to stop colliding.
|
|
222
|
+
const padX = 12;
|
|
223
|
+
const padY = a.tier === b.tier ? 8 : 2;
|
|
224
|
+
const needX = (a.w + b.w) / 2 + padX;
|
|
225
|
+
const needY = (a.h + b.h) / 2 + padY;
|
|
226
|
+
const dx = b.x - a.x;
|
|
227
|
+
const dy = b.y - a.y;
|
|
228
|
+
const overlapX = needX - Math.abs(dx);
|
|
229
|
+
const overlapY = needY - Math.abs(dy);
|
|
230
|
+
if (overlapX <= 0 || overlapY <= 0)
|
|
231
|
+
continue;
|
|
232
|
+
// Prefer to solve a collision sideways. Moving a body up or down means
|
|
233
|
+
// moving it deeper into the plane, which eats the vertical separation
|
|
234
|
+
// between tiers; there is almost always width to spare instead.
|
|
235
|
+
let pushX = 0;
|
|
236
|
+
let pushY = 0;
|
|
237
|
+
if (overlapX / needX < (overlapY / needY) * 2.2)
|
|
238
|
+
pushX = (dx >= 0 ? 1 : -1) * overlapX * 0.5;
|
|
239
|
+
else
|
|
240
|
+
pushY = (dy >= 0 ? 1 : -1) * overlapY * 0.5;
|
|
241
|
+
const { dx: wx, dz: wz } = unproject(pushX, pushY);
|
|
242
|
+
px[i] -= wx * 0.55;
|
|
243
|
+
pz[i] -= wz * 0.55;
|
|
244
|
+
px[j] += wx * 0.55;
|
|
245
|
+
pz[j] += wz * 0.55;
|
|
246
|
+
a.x -= pushX * 0.55;
|
|
247
|
+
a.y -= pushY * 0.55;
|
|
248
|
+
b.x += pushX * 0.55;
|
|
249
|
+
b.y += pushY * 0.55;
|
|
250
|
+
touched = true;
|
|
251
|
+
moved = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!touched)
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return moved;
|
|
258
|
+
}
|
|
259
|
+
/** How far the planes reach, so the ground can be drawn under everything on it. */
|
|
260
|
+
export function worldExtent(world, margin = 0.35) {
|
|
261
|
+
let extent = 0.6;
|
|
262
|
+
for (const n of world.values())
|
|
263
|
+
extent = Math.max(extent, Math.abs(n.x), Math.abs(n.z));
|
|
264
|
+
return extent + margin;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Push apart anything closer than the room a body needs.
|
|
268
|
+
*
|
|
269
|
+
* The target gap comes from how many things share the plane: a dozen services
|
|
270
|
+
* on one plane need to be further apart in world units than three do, because
|
|
271
|
+
* they are competing for the same projected area.
|
|
272
|
+
*/
|
|
273
|
+
function separate(px, pz, tiers, rand, tilt, gap) {
|
|
274
|
+
const n = px.length;
|
|
275
|
+
// Separation has to be measured in the space the viewer sees, not the space
|
|
276
|
+
// the nodes live in. The projection squashes the depth axis by `tilt`, so two
|
|
277
|
+
// bodies a comfortable distance apart in z land almost on top of each other
|
|
278
|
+
// on screen. Measuring with z pre-squashed is what makes the plane read as a
|
|
279
|
+
// plane instead of a pile.
|
|
280
|
+
const squash = Math.max(tilt, 0.15);
|
|
281
|
+
for (let pass = 0; pass < 90; pass++) {
|
|
282
|
+
let worst = 0;
|
|
283
|
+
for (let i = 0; i < n; i++) {
|
|
284
|
+
for (let j = i + 1; j < n; j++) {
|
|
285
|
+
const sameTier = tiers[i] === tiers[j];
|
|
286
|
+
// Different planes still collide on screen, just less severely.
|
|
287
|
+
const want = sameTier ? gap : gap * 0.3;
|
|
288
|
+
let dx = px[j] - px[i];
|
|
289
|
+
let dz = (pz[j] - pz[i]) * squash;
|
|
290
|
+
let d = Math.hypot(dx, dz);
|
|
291
|
+
if (d < 1e-5) {
|
|
292
|
+
dx = rand() - 0.5;
|
|
293
|
+
dz = rand() - 0.5;
|
|
294
|
+
d = Math.hypot(dx, dz) || 1;
|
|
295
|
+
}
|
|
296
|
+
if (d >= want)
|
|
297
|
+
continue;
|
|
298
|
+
const push = ((want - d) / d) * 0.5;
|
|
299
|
+
px[i] -= dx * push;
|
|
300
|
+
pz[i] -= (dz / squash) * push;
|
|
301
|
+
px[j] += dx * push;
|
|
302
|
+
pz[j] += (dz / squash) * push;
|
|
303
|
+
worst = Math.max(worst, want - d);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (worst < 0.004)
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Fit the projected world into a box, leaving room for labels under each body. */
|
|
311
|
+
export function fitCamera(world, width, height, yaw, tilt = 0.3, padding = 44,
|
|
312
|
+
/** Include the tier planes in the fit, so the ground is not clipped. */
|
|
313
|
+
extent) {
|
|
314
|
+
const probe = { yaw, tilt, scale: 1, scaleY: 1, cx: 0, cy: 0 };
|
|
315
|
+
let minX = Infinity;
|
|
316
|
+
let maxX = -Infinity;
|
|
317
|
+
let minY = Infinity;
|
|
318
|
+
let maxY = -Infinity;
|
|
319
|
+
const ys = [...world.values()].map((n) => n.y);
|
|
320
|
+
const probes = [...world.values()].map((n) => ({ x: n.x, y: n.y, z: n.z }));
|
|
321
|
+
if (extent && ys.length) {
|
|
322
|
+
for (const y of [Math.min(...ys), Math.max(...ys)]) {
|
|
323
|
+
for (const sx of [-extent, extent])
|
|
324
|
+
for (const sz of [-extent, extent])
|
|
325
|
+
probes.push({ x: sx, y, z: sz });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const node of probes) {
|
|
329
|
+
const p = project3(node, probe);
|
|
330
|
+
minX = Math.min(minX, p.x);
|
|
331
|
+
maxX = Math.max(maxX, p.x);
|
|
332
|
+
minY = Math.min(minY, p.y);
|
|
333
|
+
maxY = Math.max(maxY, p.y);
|
|
334
|
+
}
|
|
335
|
+
if (!Number.isFinite(minX)) {
|
|
336
|
+
return { yaw, tilt, scale: 100, scaleY: 100, cx: width / 2, cy: height / 2 };
|
|
337
|
+
}
|
|
338
|
+
const boxW = Math.max(width - padding * 2, 120);
|
|
339
|
+
const boxH = Math.max(height - padding * 2 - 44, 120);
|
|
340
|
+
const fitX = boxW / Math.max(maxX - minX, 0.001);
|
|
341
|
+
const fitY = boxH / Math.max(maxY - minY, 0.001);
|
|
342
|
+
const base = Math.min(fitX, fitY);
|
|
343
|
+
const MAX_STRETCH = 1.6;
|
|
344
|
+
const scale = Math.min(fitX, base * MAX_STRETCH);
|
|
345
|
+
const scaleY = Math.min(fitY, base * MAX_STRETCH);
|
|
346
|
+
const midX = (minX + maxX) / 2;
|
|
347
|
+
const midY = (minY + maxY) / 2;
|
|
348
|
+
return { yaw, tilt, scale, scaleY, cx: width / 2 - midX * scale, cy: height / 2 - midY * scaleY };
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=layout3d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout3d.js","sourceRoot":"","sources":["../src/layout3d.ts"],"names":[],"mappings":"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"}
|
package/dist/mesh.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FlowTable } from './model.js';
|
|
2
|
+
export interface MeshOptions {
|
|
3
|
+
seed?: number;
|
|
4
|
+
services?: number;
|
|
5
|
+
/** Environments, in the order you would promote through them. */
|
|
6
|
+
envs?: string[];
|
|
7
|
+
regions?: string[];
|
|
8
|
+
/** Baseline requests per second entering prod in the primary region. */
|
|
9
|
+
baseRps?: number;
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A deliberately messy but realistic mesh.
|
|
14
|
+
*
|
|
15
|
+
* Every knob the framework exposes is present in the data: services own several
|
|
16
|
+
* APIs, the same API runs in several environments and regions, dependencies
|
|
17
|
+
* declare whether the caller fails open or closed on them, and every thing
|
|
18
|
+
* carries an engine, a team, an instance count and live CPU and memory. That is
|
|
19
|
+
* the point - a demo where each dimension has exactly one plausible use teaches
|
|
20
|
+
* you nothing about choosing between them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateMesh(options?: MeshOptions): FlowTable;
|
|
23
|
+
//# sourceMappingURL=mesh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh.d.ts","sourceRoot":"","sources":["../src/mesh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,SAAS,EAAY,MAAM,YAAY,CAAC;AAGlE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4DD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,WAAgB,GAAG,SAAS,CAqNjE"}
|
package/dist/mesh.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { mulberry32 } from './particles.js';
|
|
2
|
+
// Three teams. Each service, and the store it owns, belongs to exactly one, so
|
|
3
|
+
// "group by team" draws three neighbourhoods rather than a scatter.
|
|
4
|
+
const CATALOG = [
|
|
5
|
+
['orders', 'Payments', ['POST /orders', 'GET /orders/{id}', 'POST /orders/{id}/cancel']],
|
|
6
|
+
['pricing', 'Payments', ['GET /quote', 'POST /quote/bulk']],
|
|
7
|
+
['catalog', 'Identity', ['GET /items/{sku}', 'GET /items:search', 'POST /items']],
|
|
8
|
+
['identity', 'Identity', ['POST /token', 'GET /me', 'POST /introspect']],
|
|
9
|
+
['payments', 'Payments', ['POST /charges', 'POST /refunds', 'GET /charges/{id}']],
|
|
10
|
+
['shipping', 'Payments', ['POST /shipments', 'GET /rates']],
|
|
11
|
+
['inventory', 'Risk', ['GET /stock/{sku}', 'POST /reserve']],
|
|
12
|
+
['fraud', 'Risk', ['POST /score']],
|
|
13
|
+
['notify', 'Identity', ['POST /send', 'GET /prefs']],
|
|
14
|
+
['reviews', 'Risk', ['GET /reviews/{sku}', 'POST /reviews']],
|
|
15
|
+
['ledger', 'Payments', ['POST /entries', 'GET /balance']],
|
|
16
|
+
['search', 'Identity', ['GET /search', 'POST /index']],
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* A store has no APIs, but every record needs one on both ends. Name the
|
|
20
|
+
* operation the engine serves rather than a placeholder, so a box expanded
|
|
21
|
+
* into its APIs reads "orders-db / query", not "orders-db / primary".
|
|
22
|
+
*/
|
|
23
|
+
function storeOp(engine) {
|
|
24
|
+
switch (engine) {
|
|
25
|
+
case 'redis':
|
|
26
|
+
case 'dynamo':
|
|
27
|
+
return 'get';
|
|
28
|
+
case 'kafka':
|
|
29
|
+
return 'produce';
|
|
30
|
+
case 's3':
|
|
31
|
+
return 'put';
|
|
32
|
+
default:
|
|
33
|
+
return 'query';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const STORES = [
|
|
37
|
+
['orders-db', 'postgres'],
|
|
38
|
+
['pricing-cache', 'redis'],
|
|
39
|
+
['catalog-db', 'postgres'],
|
|
40
|
+
['identity-db', 'postgres'],
|
|
41
|
+
['payments-db', 'postgres'],
|
|
42
|
+
['shipping-db', 'postgres'],
|
|
43
|
+
['inventory-kv', 'dynamo'],
|
|
44
|
+
['fraud-features', 'cassandra'],
|
|
45
|
+
['notify-queue', 'kafka'],
|
|
46
|
+
['reviews-blob', 's3'],
|
|
47
|
+
['ledger-db', 'postgres'],
|
|
48
|
+
['search-index', 'clickhouse'],
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* A deliberately messy but realistic mesh.
|
|
52
|
+
*
|
|
53
|
+
* Every knob the framework exposes is present in the data: services own several
|
|
54
|
+
* APIs, the same API runs in several environments and regions, dependencies
|
|
55
|
+
* declare whether the caller fails open or closed on them, and every thing
|
|
56
|
+
* carries an engine, a team, an instance count and live CPU and memory. That is
|
|
57
|
+
* the point - a demo where each dimension has exactly one plausible use teaches
|
|
58
|
+
* you nothing about choosing between them.
|
|
59
|
+
*/
|
|
60
|
+
export function generateMesh(options = {}) {
|
|
61
|
+
const { seed = 21, services = 12, envs = ['prod', 'staging', 'dev'], regions = ['eu-west', 'us-east'], baseRps = 6400, title = 'Messy mesh: 12 services, ~40 APIs, 3 environments, 2 regions', } = options;
|
|
62
|
+
const rand = mulberry32(seed);
|
|
63
|
+
const between = (lo, hi) => lo + rand() * (hi - lo);
|
|
64
|
+
const specs = CATALOG.slice(0, services).map(([name, team, apis], i) => ({
|
|
65
|
+
name,
|
|
66
|
+
team,
|
|
67
|
+
apis,
|
|
68
|
+
store: { name: STORES[i % STORES.length][0], engine: STORES[i % STORES.length][1] },
|
|
69
|
+
}));
|
|
70
|
+
// Only prod runs everywhere. Lower environments live in the primary region,
|
|
71
|
+
// which is exactly the asymmetry that makes region worth being a dimension.
|
|
72
|
+
const deployments = [];
|
|
73
|
+
for (const env of envs) {
|
|
74
|
+
for (const region of regions) {
|
|
75
|
+
if (env !== 'prod' && region !== regions[0])
|
|
76
|
+
continue;
|
|
77
|
+
const share = env === 'prod' ? (region === regions[0] ? 1 : 0.62) : env === 'staging' ? 0.06 : 0.015;
|
|
78
|
+
deployments.push({ env, region, share });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const records = [];
|
|
82
|
+
const nodes = [];
|
|
83
|
+
// Errors are not uniform. A healthy fleet sits well under 0.5%; one service
|
|
84
|
+
// is having a bad day (a few percent) and one busy API is on fire (double
|
|
85
|
+
// digits). The halo channel reads error rate on an absolute scale so those
|
|
86
|
+
// two stand out - which means the seed has to contain them. Picked once, so
|
|
87
|
+
// the same things are broken in both regions, as a real incident would be.
|
|
88
|
+
const errPick = specs.filter((s) => !['identity', 'orders', 'payments'].includes(s.name));
|
|
89
|
+
const sick = errPick[Math.floor(rand() * errPick.length)].name;
|
|
90
|
+
let burning = errPick[Math.floor(rand() * errPick.length)];
|
|
91
|
+
if (burning.name === sick)
|
|
92
|
+
burning = errPick[(errPick.indexOf(burning) + 1) % errPick.length];
|
|
93
|
+
const burningApi = burning.apis[0];
|
|
94
|
+
const push = (from, to, dims, rps, latencyMs, errorRate, bytes) => {
|
|
95
|
+
records.push({
|
|
96
|
+
from,
|
|
97
|
+
to,
|
|
98
|
+
dims,
|
|
99
|
+
metrics: {
|
|
100
|
+
rps: Math.round(rps * 100) / 100,
|
|
101
|
+
latencyMs: Math.round(latencyMs * 10) / 10,
|
|
102
|
+
errorRate: Math.round(errorRate * 10000) / 10000,
|
|
103
|
+
bytes: Math.round(bytes),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
for (const { env, region, share } of deployments) {
|
|
108
|
+
const scale = baseRps * share;
|
|
109
|
+
// Edge: clients -> gateway -> each public API.
|
|
110
|
+
push({ service: 'clients', api: 'browser', kind: 'client' }, { service: 'gateway', api: 'ingress', kind: 'gateway' }, { env, region, failure: 'fail-closed', protocol: 'https' }, scale, between(1.5, 3), between(0, 0.002), 820);
|
|
111
|
+
const publicApis = specs.flatMap((s) => s.apis.slice(0, 2).map((api) => ({ service: s.name, api })));
|
|
112
|
+
// Log-normal, not uniform: a handful of APIs carry most of the traffic and
|
|
113
|
+
// a long tail carries almost none, spanning two to three orders of
|
|
114
|
+
// magnitude. That is what production looks like, and it is the contrast
|
|
115
|
+
// the density channel exists to show.
|
|
116
|
+
const weights = publicApis.map(() => Math.exp(between(-2.6, 2.6)));
|
|
117
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
118
|
+
// The gateway is hard-wired to the services a request cannot complete
|
|
119
|
+
// without, and deliberately soft on the rest. Getting this wrong - failing
|
|
120
|
+
// closed on everything - is what turns one dead service into a dead site,
|
|
121
|
+
// and the blast-radius view exists to show exactly that.
|
|
122
|
+
const CRITICAL = new Set(['identity', 'orders', 'payments']);
|
|
123
|
+
// Errors are not uniform either. A healthy fleet sits well under 0.5%;
|
|
124
|
+
// one service is having a bad day (a few percent), and one API is on
|
|
125
|
+
// fire (double digits). The halo channel is built to make those two
|
|
126
|
+
// stand out at an absolute scale, so the seed has to contain them.
|
|
127
|
+
const errorFor = (service, api, healthy) => {
|
|
128
|
+
if (env !== 'prod')
|
|
129
|
+
return healthy;
|
|
130
|
+
if (service === burning.name && api === burningApi)
|
|
131
|
+
return between(0.12, 0.3);
|
|
132
|
+
if (service === sick)
|
|
133
|
+
return between(0.02, 0.05);
|
|
134
|
+
return healthy;
|
|
135
|
+
};
|
|
136
|
+
publicApis.forEach((target, i) => {
|
|
137
|
+
push({ service: 'gateway', api: 'ingress', kind: 'gateway' }, { ...target, kind: 'service' }, {
|
|
138
|
+
env,
|
|
139
|
+
region,
|
|
140
|
+
failure: CRITICAL.has(target.service) ? 'fail-closed' : 'fail-open',
|
|
141
|
+
protocol: 'https',
|
|
142
|
+
}, (scale * weights[i]) / total, between(4, 40), errorFor(target.service, target.api, between(0, 0.004)), 900);
|
|
143
|
+
});
|
|
144
|
+
for (const spec of specs) {
|
|
145
|
+
for (const api of spec.apis) {
|
|
146
|
+
const inbound = records.find((r) => r.to.service === spec.name && r.to.api === api && r.dims?.env === env && r.dims?.region === region)
|
|
147
|
+
?.metrics.rps ?? scale * Math.exp(between(-6, -2.5));
|
|
148
|
+
// Every API touches its own store.
|
|
149
|
+
push({ service: spec.name, api, kind: 'service' }, { service: spec.store.name, api: storeOp(spec.store.engine), kind: storeKind(spec.store.engine), engine: spec.store.engine }, { env, region, failure: 'fail-closed', protocol: engineProtocol(spec.store.engine) }, inbound * between(0.9, 2.4), storeLatency(spec.store.engine, between(0.6, 1.4)), between(0, 0.003), between(300, 5200));
|
|
150
|
+
// Cross-service calls, each with its own failure contract. Most
|
|
151
|
+
// enrichment is genuinely optional, so fail-open is the common case -
|
|
152
|
+
// and those links are the firebreaks that keep one dead service from
|
|
153
|
+
// taking the estate with it.
|
|
154
|
+
const fanout = rand() < 0.55 ? 1 : rand() < 0.8 ? 2 : 0;
|
|
155
|
+
for (let k = 0; k < fanout; k++) {
|
|
156
|
+
const other = specs[Math.floor(rand() * specs.length)];
|
|
157
|
+
if (other.name === spec.name)
|
|
158
|
+
continue;
|
|
159
|
+
const otherApi = other.apis[Math.floor(rand() * other.apis.length)];
|
|
160
|
+
const failOpen = rand() < 0.66;
|
|
161
|
+
push({ service: spec.name, api, kind: 'service' }, { service: other.name, api: otherApi, kind: 'service' }, {
|
|
162
|
+
env,
|
|
163
|
+
region,
|
|
164
|
+
failure: failOpen ? 'fail-open' : 'fail-closed',
|
|
165
|
+
protocol: rand() < 0.7 ? 'grpc' : 'https',
|
|
166
|
+
}, inbound * between(0.1, 0.9), between(3, 70), errorFor(other.name, otherApi, failOpen ? between(0, 0.008) : between(0, 0.004)), between(200, 2600));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Per-thing facts: instances, and how hot they are running.
|
|
171
|
+
for (const spec of specs) {
|
|
172
|
+
const instances = env === 'prod' ? Math.round(between(4, 48)) : env === 'staging' ? 2 : 1;
|
|
173
|
+
nodes.push({
|
|
174
|
+
key: { service: spec.name },
|
|
175
|
+
dims: { env, region, team: spec.team, kind: 'service' },
|
|
176
|
+
attrs: {
|
|
177
|
+
instances,
|
|
178
|
+
cpu: clamp01(between(0.18, env === 'prod' ? 0.94 : 0.4)),
|
|
179
|
+
mem: clamp01(between(0.3, env === 'prod' ? 0.88 : 0.5)),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
nodes.push({
|
|
183
|
+
key: { service: spec.store.name },
|
|
184
|
+
dims: { env, region, engine: spec.store.engine, team: spec.team, kind: storeKind(spec.store.engine) },
|
|
185
|
+
component: componentFor(spec.store.engine),
|
|
186
|
+
attrs: {
|
|
187
|
+
instances: env === 'prod' ? Math.round(between(3, 12)) : 1,
|
|
188
|
+
cpu: clamp01(between(0.25, 0.9)),
|
|
189
|
+
mem: clamp01(between(0.4, 0.95)),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
nodes.push({
|
|
194
|
+
key: { service: 'gateway' },
|
|
195
|
+
dims: { env, region, team: 'Identity', kind: 'gateway' },
|
|
196
|
+
attrs: { instances: env === 'prod' ? 24 : 2, cpu: clamp01(between(0.3, 0.8)), mem: clamp01(between(0.3, 0.7)) },
|
|
197
|
+
});
|
|
198
|
+
nodes.push({
|
|
199
|
+
key: { service: 'clients' },
|
|
200
|
+
dims: { env, region, team: 'Outside', kind: 'client' },
|
|
201
|
+
attrs: { instances: 1, cpu: 0, mem: 0 },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
records,
|
|
207
|
+
nodes,
|
|
208
|
+
meta: {
|
|
209
|
+
title,
|
|
210
|
+
window: 'synthetic',
|
|
211
|
+
note: 'Every dimension here is a knob: service, api, env, region, team, engine, protocol, failure.',
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function storeKind(engine) {
|
|
216
|
+
if (/redis|memcache/.test(engine))
|
|
217
|
+
return 'cache';
|
|
218
|
+
if (/kafka|sqs|rabbit/.test(engine))
|
|
219
|
+
return 'queue';
|
|
220
|
+
return 'datastore';
|
|
221
|
+
}
|
|
222
|
+
function componentFor(engine) {
|
|
223
|
+
if (/postgres|mysql|aurora/.test(engine))
|
|
224
|
+
return 'db-cylinder';
|
|
225
|
+
if (/cassandra|clickhouse|bigtable/.test(engine))
|
|
226
|
+
return 'db-discs';
|
|
227
|
+
if (/dynamo|rocks|kv/.test(engine))
|
|
228
|
+
return 'db-cube';
|
|
229
|
+
if (/kafka|pulsar|kinesis/.test(engine))
|
|
230
|
+
return 'db-drum';
|
|
231
|
+
if (/redis|memcache/.test(engine))
|
|
232
|
+
return 'cache-chip';
|
|
233
|
+
if (/s3|gcs|blob/.test(engine))
|
|
234
|
+
return 'blob-bucket';
|
|
235
|
+
return 'db-cylinder';
|
|
236
|
+
}
|
|
237
|
+
function engineProtocol(engine) {
|
|
238
|
+
if (/redis/.test(engine))
|
|
239
|
+
return 'resp';
|
|
240
|
+
if (/kafka/.test(engine))
|
|
241
|
+
return 'kafka';
|
|
242
|
+
if (/s3/.test(engine))
|
|
243
|
+
return 'https';
|
|
244
|
+
return 'sql';
|
|
245
|
+
}
|
|
246
|
+
function storeLatency(engine, jitter) {
|
|
247
|
+
const base = /redis|memcache/.test(engine)
|
|
248
|
+
? 0.7
|
|
249
|
+
: /dynamo|kv/.test(engine)
|
|
250
|
+
? 4
|
|
251
|
+
: /kafka/.test(engine)
|
|
252
|
+
? 2.5
|
|
253
|
+
: /s3|blob/.test(engine)
|
|
254
|
+
? 38
|
|
255
|
+
: /clickhouse|cassandra/.test(engine)
|
|
256
|
+
? 22
|
|
257
|
+
: 9;
|
|
258
|
+
return base * jitter;
|
|
259
|
+
}
|
|
260
|
+
function clamp01(v) {
|
|
261
|
+
return Math.round(Math.max(0, Math.min(1, v)) * 100) / 100;
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=mesh.js.map
|