@tellegen/svelte 0.4.3 → 0.4.5

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 (115) hide show
  1. package/README.md +45 -0
  2. package/dist/TellegenMap.svelte +63 -22
  3. package/dist/TellegenProvider.svelte +7 -4
  4. package/dist/TellegenShell.svelte +12 -1
  5. package/dist/TellegenViewer.svelte +2 -0
  6. package/dist/basemap.d.ts +14 -0
  7. package/dist/basemap.js +27 -0
  8. package/dist/colors.d.ts +6 -1
  9. package/dist/colors.js +11 -4
  10. package/dist/components/DiagnosticLogView.svelte +176 -0
  11. package/dist/components/DiagnosticLogView.svelte.d.ts +11 -0
  12. package/dist/components/FormulationSelector.svelte +1 -1
  13. package/dist/components/LocalCaseDetails.svelte +18 -3
  14. package/dist/components/LogEntryView.svelte +188 -0
  15. package/dist/components/LogEntryView.svelte.d.ts +11 -0
  16. package/dist/components/LogPanel.svelte +25 -0
  17. package/dist/components/LogPanel.svelte.d.ts +3 -0
  18. package/dist/components/McResults.svelte +18 -3
  19. package/dist/components/McWorkbench.svelte +688 -0
  20. package/dist/components/McWorkbench.svelte.d.ts +38 -0
  21. package/dist/components/MultiWire.svelte +136 -0
  22. package/dist/components/MultiWire.svelte.d.ts +7 -0
  23. package/dist/components/MulticonductorDetails.svelte +7 -0
  24. package/dist/components/NotificationCenter.svelte +37 -6
  25. package/dist/components/OmniSearch.svelte +163 -0
  26. package/dist/components/OmniSearch.svelte.d.ts +14 -0
  27. package/dist/components/ResultsTable.svelte +259 -0
  28. package/dist/components/ResultsTable.svelte.d.ts +13 -0
  29. package/dist/components/SchematicCanvas.svelte +1896 -0
  30. package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
  31. package/dist/components/SolveCard.svelte +6 -0
  32. package/dist/components/schematic/PickCard.svelte +126 -0
  33. package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
  34. package/dist/components/schematic/export.d.ts +21 -0
  35. package/dist/components/schematic/export.js +110 -0
  36. package/dist/components/schematic/layers.d.ts +161 -0
  37. package/dist/components/schematic/layers.js +756 -0
  38. package/dist/components/schematic/paint.d.ts +28 -0
  39. package/dist/components/schematic/paint.js +69 -0
  40. package/dist/components/schematic/palette.d.ts +34 -0
  41. package/dist/components/schematic/palette.js +54 -0
  42. package/dist/components/schematic/text.d.ts +83 -0
  43. package/dist/components/schematic/text.js +190 -0
  44. package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
  45. package/dist/components/schematic/view-prefs.svelte.js +77 -0
  46. package/dist/components.d.ts +4 -0
  47. package/dist/components.js +4 -0
  48. package/dist/context.svelte.d.ts +2 -0
  49. package/dist/context.svelte.js +2 -0
  50. package/dist/controller.svelte.d.ts +40 -5
  51. package/dist/controller.svelte.js +230 -25
  52. package/dist/diagram/basemap.d.ts +28 -0
  53. package/dist/diagram/basemap.js +39 -0
  54. package/dist/diagram/bus-stacks.d.ts +19 -0
  55. package/dist/diagram/bus-stacks.js +99 -0
  56. package/dist/diagram/colour-modes.d.ts +18 -0
  57. package/dist/diagram/colour-modes.js +33 -0
  58. package/dist/diagram/connections.d.ts +30 -0
  59. package/dist/diagram/connections.js +129 -0
  60. package/dist/diagram/display-tree.d.ts +21 -0
  61. package/dist/diagram/display-tree.js +65 -0
  62. package/dist/diagram/energisation.d.ts +10 -0
  63. package/dist/diagram/energisation.js +31 -0
  64. package/dist/diagram/feeders.d.ts +30 -0
  65. package/dist/diagram/feeders.js +57 -0
  66. package/dist/diagram/galvanic-zones.d.ts +60 -0
  67. package/dist/diagram/galvanic-zones.js +210 -0
  68. package/dist/diagram/graph-inputs.d.ts +18 -0
  69. package/dist/diagram/graph-inputs.js +103 -0
  70. package/dist/diagram/index.d.ts +28 -0
  71. package/dist/diagram/index.js +28 -0
  72. package/dist/diagram/landmarks.d.ts +50 -0
  73. package/dist/diagram/landmarks.js +153 -0
  74. package/dist/diagram/layout-morph.d.ts +15 -0
  75. package/dist/diagram/layout-morph.js +32 -0
  76. package/dist/diagram/multi-wire.d.ts +44 -0
  77. package/dist/diagram/multi-wire.js +65 -0
  78. package/dist/diagram/phase-bars.d.ts +35 -0
  79. package/dist/diagram/phase-bars.js +44 -0
  80. package/dist/diagram/results-table.d.ts +47 -0
  81. package/dist/diagram/results-table.js +173 -0
  82. package/dist/diagram/schematic-equipment.d.ts +133 -0
  83. package/dist/diagram/schematic-equipment.js +645 -0
  84. package/dist/diagram/schematic-layout.d.ts +51 -0
  85. package/dist/diagram/schematic-layout.js +240 -0
  86. package/dist/diagram/search.d.ts +36 -0
  87. package/dist/diagram/search.js +48 -0
  88. package/dist/diagram/shared-routes.d.ts +21 -0
  89. package/dist/diagram/shared-routes.js +40 -0
  90. package/dist/diagram/short-names.d.ts +11 -0
  91. package/dist/diagram/short-names.js +54 -0
  92. package/dist/diagram/snapshot.d.ts +115 -0
  93. package/dist/diagram/snapshot.js +242 -0
  94. package/dist/diagram/summary.d.ts +65 -0
  95. package/dist/diagram/summary.js +144 -0
  96. package/dist/diagram/svg-export.d.ts +80 -0
  97. package/dist/diagram/svg-export.js +108 -0
  98. package/dist/diagram/tree-walk.d.ts +13 -0
  99. package/dist/diagram/tree-walk.js +83 -0
  100. package/dist/diagram/value-labels.d.ts +24 -0
  101. package/dist/diagram/value-labels.js +69 -0
  102. package/dist/diagram/view-state.d.ts +12 -0
  103. package/dist/diagram/view-state.js +22 -0
  104. package/dist/diagram/voltage-levels.d.ts +14 -0
  105. package/dist/diagram/voltage-levels.js +20 -0
  106. package/dist/index.d.ts +3 -0
  107. package/dist/index.js +2 -0
  108. package/dist/log.svelte.d.ts +77 -0
  109. package/dist/log.svelte.js +149 -0
  110. package/dist/notices.svelte.d.ts +3 -0
  111. package/dist/state.svelte.d.ts +42 -5
  112. package/dist/state.svelte.js +56 -4
  113. package/dist/transformer-icon.d.ts +22 -13
  114. package/dist/transformer-icon.js +23 -13
  115. package/package.json +2 -2
@@ -0,0 +1,756 @@
1
+ import { branchGlyph, conductorSet, conductorTicks, routeMiddle } from '../../diagram/index.js';
2
+ import { hiddenUnderFan, landmarkColours, segmentColours, segmentWidths, strokeColours } from './paint.js';
3
+ import { ACCENT, CORRIDOR_PX, INK, MONO, PAPER, PATH, SYMBOL_INK, ZONE_COLOURS, ZONE_INK, alpha, lineWidths, stateInk } from './palette.js';
4
+ /** Screen radius of a spread stack of `n` buses: wider for more, so their symbols stay apart. */
5
+ export const stackRadius = (n) => 8 + 4 * n;
6
+ /** Pixel size of the paper behind text. */
7
+ const paperText = (a) => alpha(PAPER, a);
8
+ /** Screen width of the hover halos: wide enough to read under MV and LV lines alike. */
9
+ const HALO_PX = 10;
10
+ const cartesian = (kit) => ({ coordinateSystem: kit.core.COORDINATE_SYSTEM.CARTESIAN });
11
+ /** Buses gliding between layouts along straight chords. */
12
+ export function morphLayers(kit, morph, busCount) {
13
+ const common = cartesian(kit);
14
+ return [
15
+ new kit.layers.LineLayer({
16
+ ...common,
17
+ id: 'morph-chords',
18
+ data: {
19
+ length: morph.endpoints.length / 2,
20
+ attributes: {
21
+ getSourcePosition: { value: morph.s, size: 3 },
22
+ getTargetPosition: { value: morph.t, size: 3 }
23
+ }
24
+ },
25
+ getColor: [120, 114, 102, 200],
26
+ getWidth: 1,
27
+ widthUnits: 'pixels'
28
+ }),
29
+ new kit.layers.ScatterplotLayer({
30
+ ...common,
31
+ id: 'morph-buses',
32
+ data: { length: busCount, attributes: { getPosition: { value: morph.cur, size: 3 } } },
33
+ getFillColor: INK,
34
+ getRadius: 2,
35
+ radiusUnits: 'pixels'
36
+ })
37
+ ];
38
+ }
39
+ /** The last value for the last keys, compared by identity. */
40
+ function keyed() {
41
+ let last = null;
42
+ let value;
43
+ return (keys, compute) => {
44
+ if (!last || keys.length !== last.length || keys.some((k, i) => k !== last[i])) {
45
+ value = compute();
46
+ last = keys;
47
+ }
48
+ return value;
49
+ };
50
+ }
51
+ export function createLayerCache() {
52
+ return {
53
+ branches: keyed(),
54
+ underlay: keyed(),
55
+ equipment: keyed(),
56
+ ticks: keyed(),
57
+ arrows: keyed(),
58
+ itemTexts: keyed(),
59
+ transformerTexts: keyed(),
60
+ zoneTags: keyed(),
61
+ corridors: keyed(),
62
+ segmentsOf: keyed()
63
+ };
64
+ }
65
+ export function sceneLayers(kit, s) {
66
+ const { LineLayer, ScatterplotLayer, TextLayer, PolygonLayer } = kit.layers;
67
+ const common = cartesian(kit);
68
+ const g = s.geometry;
69
+ const d = s.drawn;
70
+ const cache = s.cache;
71
+ const segmentsOf = cache.segmentsOf([d], () => segmentIndex(d));
72
+ const point = (i) => [
73
+ g.positions[i * 3],
74
+ g.positions[i * 3 + 1],
75
+ 0
76
+ ];
77
+ const segment = (i, end) => [
78
+ end[i * 3],
79
+ end[i * 3 + 1],
80
+ 0
81
+ ];
82
+ const symbols = s.symbols;
83
+ return [
84
+ // Zone outlines sit beneath everything; they pick only where nothing else is.
85
+ ...corridorLayers(kit, s),
86
+ ...hoverHalos(kit, s),
87
+ new PolygonLayer({
88
+ ...common,
89
+ id: 'zones',
90
+ data: s.zones.shapes,
91
+ getPolygon: (z) => z.polygon,
92
+ getFillColor: (z) => ZONE_COLOURS[s.zones.states[z.z]][0],
93
+ getLineColor: (z) => ZONE_COLOURS[s.zones.states[z.z]][1],
94
+ getLineWidth: 1,
95
+ lineWidthUnits: 'pixels',
96
+ lineWidthMinPixels: 1,
97
+ pickable: true,
98
+ updateTriggers: { getFillColor: s.zones.states, getLineColor: s.zones.states }
99
+ }),
100
+ new TextLayer({
101
+ ...common,
102
+ id: 'zone-tags',
103
+ data: cache.zoneTags([s.zones.tags, s.zones.tagZoom, s.layout === 'source' && !!symbols], () => s.layout === 'source' && symbols
104
+ ? []
105
+ : s.zones.tags.filter((t) => t.width * 2 ** s.zones.tagZoom >= 150)),
106
+ getPosition: (t) => [...t.position, 0],
107
+ getText: (t) => t.text,
108
+ getColor: (t) => {
109
+ const state = s.zones.states[t.z];
110
+ return state === 'violation' || state === 'warning' ? stateInk(state) : ZONE_INK;
111
+ },
112
+ getSize: 10,
113
+ getTextAnchor: 'start',
114
+ getAlignmentBaseline: 'bottom',
115
+ getPixelOffset: [2, -2],
116
+ background: true,
117
+ getBackgroundColor: paperText(220),
118
+ backgroundPadding: [3, 1],
119
+ fontFamily: MONO,
120
+ characterSet: 'auto',
121
+ updateTriggers: { getColor: s.zones.states }
122
+ }),
123
+ // Result colours are quiet for normal values; a faint underlay keeps the topology legible.
124
+ ...(s.colour === 'none'
125
+ ? []
126
+ : [
127
+ new LineLayer({
128
+ ...common,
129
+ id: 'topology-underlay',
130
+ data: cache.underlay([d], () => ({
131
+ length: d.segmentEdge.length,
132
+ attributes: {
133
+ getSourcePosition: { value: d.sources, size: 3 },
134
+ getTargetPosition: { value: d.targets, size: 3 }
135
+ }
136
+ })),
137
+ getColor: [120, 114, 102, 70],
138
+ getWidth: 1,
139
+ widthUnits: 'pixels'
140
+ })
141
+ ]),
142
+ new LineLayer({
143
+ ...common,
144
+ id: `branches-${s.layout}`,
145
+ data: cache.branches([d, s.edgeColours, s.edgeMv, s.zoomedOut, s.fan, s.colour !== 'none'], () => ({
146
+ length: d.segmentEdge.length,
147
+ attributes: {
148
+ getSourcePosition: { value: d.sources, size: 3 },
149
+ getTargetPosition: { value: d.targets, size: 3 },
150
+ getColor: {
151
+ value: segmentColours(d, s.edgeColours, s.edgeMv, s.zoomedOut, s.fan),
152
+ size: 4,
153
+ normalized: true
154
+ },
155
+ getWidth: { value: segmentWidths(d, s.edgeMv, s.colour !== 'none'), size: 1 }
156
+ }
157
+ })),
158
+ widthUnits: 'pixels',
159
+ pickable: true
160
+ }),
161
+ new LineLayer({
162
+ ...common,
163
+ id: 'selected-branch',
164
+ data: highlighted(d, segmentsOf, s.selectedEdges, s.fan),
165
+ getSourcePosition: (i) => segment(i, d.sources),
166
+ getTargetPosition: (i) => segment(i, d.targets),
167
+ getColor: ACCENT,
168
+ getWidth: 4,
169
+ widthUnits: 'pixels'
170
+ }),
171
+ ...sharedRouteLayers(kit, s),
172
+ ...stackLayers(kit, s),
173
+ ...(symbols
174
+ ? [
175
+ new LineLayer({
176
+ ...common,
177
+ id: 'equipment',
178
+ data: cache.equipment([symbols, s.fan.edges, s.live], () => ({
179
+ length: symbols.sources.length / 3,
180
+ attributes: {
181
+ getSourcePosition: { value: symbols.sources, size: 3 },
182
+ getTargetPosition: { value: symbols.targets, size: 3 },
183
+ getColor: {
184
+ value: strokeColours(symbols.owner, s.fan.edges, s.live),
185
+ size: 4,
186
+ normalized: true
187
+ }
188
+ }
189
+ })),
190
+ getWidth: 1.25,
191
+ widthUnits: 'pixels'
192
+ }),
193
+ ...(s.valuesOn && s.arrows ? [flowArrowLayer(kit, s.arrows, s.fan.edges, cache)] : []),
194
+ ...(s.ticksOn
195
+ ? [
196
+ new LineLayer({
197
+ ...common,
198
+ id: 'conductor-ticks',
199
+ data: cache.ticks([symbols, s.fan.edges, s.live], () => ({
200
+ length: symbols.ticks.edge.length,
201
+ attributes: {
202
+ getSourcePosition: { value: symbols.ticks.sources, size: 3 },
203
+ getTargetPosition: { value: symbols.ticks.targets, size: 3 },
204
+ getColor: {
205
+ value: strokeColours(symbols.ticks.edge, s.fan.edges, s.live),
206
+ size: 4,
207
+ normalized: true
208
+ }
209
+ }
210
+ })),
211
+ getWidth: 1,
212
+ widthUnits: 'pixels'
213
+ })
214
+ ]
215
+ : []),
216
+ // Invisible discs over each symbol make it clickable and give it a tooltip.
217
+ new ScatterplotLayer({
218
+ ...common,
219
+ id: 'equipment-hits',
220
+ data: symbols.items,
221
+ getPosition: (item) => [item.x, item.y, 0],
222
+ getRadius: symbols.unit * 1.3,
223
+ radiusUnits: 'common',
224
+ getFillColor: [0, 0, 0, 1],
225
+ pickable: true
226
+ }),
227
+ new TextLayer({
228
+ ...common,
229
+ id: 'equipment-text',
230
+ data: cache.itemTexts([symbols.items, s.textSource], () => symbols.items.filter((item) => item.bus >= 0 && s.itemText(item))),
231
+ getPosition: (item) => [item.x + symbols.unit * 1.1, item.y, 0],
232
+ getText: s.itemText,
233
+ updateTriggers: { getText: [s.valuesOn, s.labelsOn, s.powers] },
234
+ getColor: SYMBOL_INK,
235
+ getSize: 10,
236
+ getTextAnchor: 'start',
237
+ getAlignmentBaseline: 'center',
238
+ // Legible where a label crosses a lead (a neutral earth beside a shunt).
239
+ background: true,
240
+ getBackgroundColor: paperText(200),
241
+ backgroundPadding: [2, 0],
242
+ fontFamily: MONO,
243
+ characterSet: 'auto'
244
+ }),
245
+ // A transformer's vector group and rating, above its symbol: below sits a centre tap.
246
+ new TextLayer({
247
+ ...common,
248
+ id: 'transformer-text',
249
+ data: cache.transformerTexts([symbols.items, s.labelsOn], () => s.labelsOn ? symbols.items.filter((item) => item.label) : []),
250
+ getPosition: (item) => [item.x, item.y, 0],
251
+ getText: (item) => item.label ?? '',
252
+ getColor: SYMBOL_INK,
253
+ getSize: 10,
254
+ getTextAnchor: 'middle',
255
+ getAlignmentBaseline: 'bottom',
256
+ getPixelOffset: [0, -12],
257
+ background: true,
258
+ getBackgroundColor: paperText(220),
259
+ backgroundPadding: [2, 1],
260
+ fontFamily: MONO,
261
+ characterSet: 'auto'
262
+ })
263
+ ]
264
+ : []),
265
+ new ScatterplotLayer({
266
+ ...common,
267
+ id: `buses-${s.layout}`,
268
+ // Zoomed out, LV bus dots are noise unless they carry the voltage colouring.
269
+ data: s.zoomedOut && s.colour !== 'voltage-band' ? s.mvBuses : s.allBuses,
270
+ getPosition: point,
271
+ getFillColor: (i) => s.busColours[i],
272
+ // Zoomed out, small dots keep dense chains from merging into bars.
273
+ getRadius: symbols ? 2.6 : 1.4,
274
+ radiusUnits: 'pixels',
275
+ radiusMinPixels: 1,
276
+ pickable: true,
277
+ updateTriggers: {
278
+ getFillColor: [s.colour, s.snapshot],
279
+ getPosition: g,
280
+ getRadius: !!symbols
281
+ }
282
+ }),
283
+ new ScatterplotLayer({
284
+ ...common,
285
+ id: 'selected-bus',
286
+ data: s.selectedBus >= 0 ? [s.selectedBus] : [],
287
+ getPosition: point,
288
+ getFillColor: [0, 0, 0, 0],
289
+ getLineColor: ACCENT,
290
+ stroked: true,
291
+ filled: false,
292
+ getRadius: 7,
293
+ radiusUnits: 'pixels',
294
+ lineWidthUnits: 'pixels',
295
+ getLineWidth: 2.5
296
+ }),
297
+ // Hovered in a linked view (profile, table) or here: a soft halo.
298
+ new ScatterplotLayer({
299
+ ...common,
300
+ id: 'hovered-bus',
301
+ data: s.hoveredBus >= 0 ? [s.hoveredBus] : [],
302
+ getPosition: point,
303
+ getFillColor: alpha(ACCENT, 60),
304
+ getRadius: 9,
305
+ radiusUnits: 'pixels'
306
+ }),
307
+ new LineLayer({
308
+ ...common,
309
+ id: 'hovered-branch',
310
+ data: highlighted(d, segmentsOf, s.hoveredEdges, s.fan),
311
+ getSourcePosition: (i) => segment(i, d.sources),
312
+ getTargetPosition: (i) => segment(i, d.targets),
313
+ getColor: alpha(ACCENT, 110),
314
+ getWidth: 7,
315
+ widthUnits: 'pixels'
316
+ }),
317
+ ...hoverMarks(kit, s),
318
+ ...landmarkLayers(kit, s),
319
+ new ScatterplotLayer({
320
+ ...common,
321
+ id: 'keyboard-cursor',
322
+ data: s.cursor >= 0 ? [s.cursor] : [],
323
+ getPosition: point,
324
+ getLineColor: INK,
325
+ stroked: true,
326
+ filled: false,
327
+ getRadius: 11,
328
+ radiusUnits: 'pixels',
329
+ lineWidthUnits: 'pixels',
330
+ getLineWidth: 1.5
331
+ }),
332
+ new TextLayer({
333
+ ...common,
334
+ id: 'labels',
335
+ data: s.labels,
336
+ getPosition: (l) => [...l.position, 0],
337
+ getText: (l) => l.text,
338
+ getColor: (l) => stateInk(l.state),
339
+ getSize: 11,
340
+ getTextAnchor: 'start',
341
+ // Above-right of the point: attachment symbols hang below buses.
342
+ getAlignmentBaseline: 'bottom',
343
+ getPixelOffset: [6, -5],
344
+ background: true,
345
+ getBackgroundColor: paperText(230),
346
+ backgroundPadding: [3, 1],
347
+ fontFamily: MONO,
348
+ characterSet: 'auto'
349
+ })
350
+ ];
351
+ }
352
+ /** Each edge's drawn segments, built once per drawing. */
353
+ function segmentIndex(d) {
354
+ const out = new Map();
355
+ for (let i = 0; i < d.segmentEdge.length; i++) {
356
+ const k = d.segmentEdge[i];
357
+ const list = out.get(k);
358
+ if (list)
359
+ list.push(i);
360
+ else
361
+ out.set(k, [i]);
362
+ }
363
+ return out;
364
+ }
365
+ /** Drawn segments of `edges`, except those replaced by a fanned route: proportional to the edges
366
+ * asked for, not to the drawing. */
367
+ function highlighted(d, segmentsOf, edges, fan) {
368
+ const out = [];
369
+ for (const k of edges)
370
+ for (const i of segmentsOf.get(k) ?? [])
371
+ if (!hiddenUnderFan(d, i, fan))
372
+ out.push(i);
373
+ return out;
374
+ }
375
+ function flowArrowLayer(kit, arrows, fannedEdges, cache) {
376
+ return new kit.layers.LineLayer({
377
+ ...cartesian(kit),
378
+ id: 'flow-arrows',
379
+ data: cache.arrows([arrows, fannedEdges], () => ({
380
+ length: arrows.edge.length,
381
+ attributes: {
382
+ getSourcePosition: { value: arrows.sources, size: 3 },
383
+ getTargetPosition: { value: arrows.targets, size: 3 },
384
+ getColor: {
385
+ value: Uint8Array.from({ length: arrows.edge.length * 4 }, (_, i) => fannedEdges.has(arrows.edge[i >> 2]) ? 0 : ACCENT[i & 3]),
386
+ size: 4,
387
+ normalized: true
388
+ }
389
+ }
390
+ })),
391
+ getWidth: 1.5,
392
+ widthUnits: 'pixels'
393
+ });
394
+ }
395
+ /** Fixed-size markers for sources, generators, IBRs and transformers while zoomed out. */
396
+ function landmarkLayers(kit, s) {
397
+ const marks = s.marks;
398
+ if (!marks?.placed.length)
399
+ return [];
400
+ const { LineLayer, ScatterplotLayer, TextLayer } = kit.layers;
401
+ const common = cartesian(kit);
402
+ return [
403
+ // A paper halo lifts each marker off the faded drawing; it is also the pick target.
404
+ new ScatterplotLayer({
405
+ ...common,
406
+ id: 'landmark-halos',
407
+ data: marks.placed,
408
+ getPosition: (l) => [l.x, l.y, 0],
409
+ // Semi-transparent: lifts the marker without blanking the drawing under it.
410
+ getFillColor: paperText(150),
411
+ getRadius: 11,
412
+ radiusUnits: 'pixels',
413
+ pickable: true
414
+ }),
415
+ new LineLayer({
416
+ ...common,
417
+ id: 'landmarks',
418
+ data: {
419
+ length: marks.strokes.owner.length,
420
+ attributes: {
421
+ getSourcePosition: { value: marks.strokes.sources, size: 3 },
422
+ getTargetPosition: { value: marks.strokes.targets, size: 3 },
423
+ getColor: { value: landmarkColours(marks), size: 4, normalized: true }
424
+ }
425
+ },
426
+ getWidth: 1.6,
427
+ widthUnits: 'pixels'
428
+ }),
429
+ new TextLayer({
430
+ ...common,
431
+ id: 'landmark-names',
432
+ data: marks.placed.filter((l) => l.kind === 'source' || l.kind === 'generator'),
433
+ getPosition: (l) => [l.x, l.y, 0],
434
+ getText: (l) => (s.markName.get(l.ids[0]) ?? l.ids[0]) + (l.ids.length > 1 ? ` +${l.ids.length - 1}` : ''),
435
+ getColor: INK,
436
+ getSize: 10,
437
+ getTextAnchor: 'middle',
438
+ getAlignmentBaseline: 'top',
439
+ getPixelOffset: [0, 14],
440
+ background: true,
441
+ getBackgroundColor: paperText(220),
442
+ backgroundPadding: [2, 1],
443
+ fontFamily: MONO,
444
+ characterSet: 'auto'
445
+ })
446
+ ];
447
+ }
448
+ /** Fanned members of shared routes (with their marks), and the count badges of the others. */
449
+ function sharedRouteLayers(kit, s) {
450
+ const eq = s.symbols;
451
+ if (!eq)
452
+ return [];
453
+ const { LineLayer, TextLayer } = kit.layers;
454
+ const common = cartesian(kit);
455
+ const src = [];
456
+ const dst = [];
457
+ const owner = [];
458
+ for (const f of s.fans)
459
+ for (let i = 1; i < f.path.length; i++) {
460
+ src.push(f.path[i - 1][0], f.path[i - 1][1], 0);
461
+ dst.push(f.path[i][0], f.path[i][1], 0);
462
+ owner.push(f.edge);
463
+ }
464
+ // Marks on the fanned paths: the branch's symbol, or its conductor marks.
465
+ const mSrc = [];
466
+ const mDst = [];
467
+ for (const f of s.fans) {
468
+ const e = s.graph.edges[f.edge];
469
+ const glyph = branchGlyph(e.kind, e.closed);
470
+ const { phases, neutral } = conductorSet(s.graph, f.edge);
471
+ const shape = glyph ?? (s.ticksOn ? conductorTicks(phases, neutral) : null);
472
+ if (!shape)
473
+ continue;
474
+ const [cos, sin] = f.dir;
475
+ const u = eq.unit * (glyph ? 0.8 : 1);
476
+ for (const line of shape)
477
+ for (let i = 1; i < line.length; i++) {
478
+ const [ax, ay] = line[i - 1];
479
+ const [bx, by] = line[i];
480
+ mSrc.push(f.mid[0] + u * (ax * cos - ay * sin), f.mid[1] + u * (ax * sin + ay * cos), 0);
481
+ mDst.push(f.mid[0] + u * (bx * cos - by * sin), f.mid[1] + u * (bx * sin + by * cos), 0);
482
+ }
483
+ }
484
+ const colours = new Uint8Array(owner.length * 4);
485
+ const widths = new Float32Array(owner.length);
486
+ const [mv, lv] = lineWidths(s.colour !== 'none');
487
+ owner.forEach((k, i) => {
488
+ const selected = s.selectedEdges.has(k);
489
+ const hovered = s.hoveredEdges.has(k);
490
+ colours.set(selected ? ACCENT : hovered ? alpha(ACCENT, 150) : s.edgeColours[k], i * 4);
491
+ widths[i] = selected ? 3.5 : hovered ? 4 : s.edgeMv[k] ? mv : lv;
492
+ });
493
+ const badges = s.ticksOn
494
+ ? eq.routeGroups.flatMap((members, g) => {
495
+ if (s.fannedGroups.includes(g))
496
+ return [];
497
+ const p = routeMiddle(members, eq.host, s.geometry);
498
+ return p ? [{ g, n: members.length, position: p }] : [];
499
+ })
500
+ : [];
501
+ return [
502
+ ...(owner.length
503
+ ? [
504
+ new LineLayer({
505
+ ...common,
506
+ id: 'fan-branches',
507
+ data: {
508
+ length: owner.length,
509
+ attributes: {
510
+ getSourcePosition: { value: Float32Array.from(src), size: 3 },
511
+ getTargetPosition: { value: Float32Array.from(dst), size: 3 },
512
+ getColor: { value: colours, size: 4, normalized: true },
513
+ getWidth: { value: widths, size: 1 }
514
+ }
515
+ },
516
+ widthUnits: 'pixels',
517
+ pickable: true
518
+ }),
519
+ new LineLayer({
520
+ ...common,
521
+ id: 'fan-marks',
522
+ data: {
523
+ length: mSrc.length / 3,
524
+ attributes: {
525
+ getSourcePosition: { value: Float32Array.from(mSrc), size: 3 },
526
+ getTargetPosition: { value: Float32Array.from(mDst), size: 3 }
527
+ }
528
+ },
529
+ getColor: SYMBOL_INK,
530
+ getWidth: 1,
531
+ widthUnits: 'pixels'
532
+ })
533
+ ]
534
+ : []),
535
+ ...(badges.length
536
+ ? [
537
+ new TextLayer({
538
+ ...common,
539
+ id: 'route-badges',
540
+ data: badges,
541
+ getPosition: (b) => [...b.position, 0],
542
+ getText: (b) => `×${b.n}`,
543
+ getColor: SYMBOL_INK,
544
+ getSize: 11,
545
+ getPixelOffset: [0, -10],
546
+ background: true,
547
+ getBackgroundColor: paperText(230),
548
+ backgroundPadding: [3, 1],
549
+ fontFamily: MONO,
550
+ characterSet: 'auto',
551
+ pickable: true
552
+ })
553
+ ]
554
+ : [])
555
+ ];
556
+ }
557
+ /** Zone corridors on geographic positions while zoomed out: a band of fixed screen width along
558
+ * each LV network's branches, with discs at its buses to round the joints, quiet for networks
559
+ * within limits and in their state's colour otherwise. A translucent band would darken wherever
560
+ * its pieces overlap, so the bands test depth strictly, all at one depth (no per-layer polygon
561
+ * offset): each pixel is tinted once. Close in, the lines speak for themselves. */
562
+ function corridorLayers(kit, s) {
563
+ const { corridors, states } = s.zones;
564
+ if (!corridors.length || s.symbols)
565
+ return [];
566
+ const { LineLayer, ScatterplotLayer } = kit.layers;
567
+ const common = cartesian(kit);
568
+ const g = s.shown;
569
+ const once = { depthWriteEnabled: true, depthCompare: 'less' };
570
+ const band = (z) => {
571
+ const state = states[z];
572
+ if (state === 'warning' || state === 'violation') {
573
+ const fill = ZONE_COLOURS[state][0];
574
+ return alpha(fill, Math.min(255, fill[3] * 3));
575
+ }
576
+ return alpha(ZONE_INK, 56);
577
+ };
578
+ const { segments, buses } = s.cache.corridors([corridors], () => ({
579
+ segments: corridors.flatMap((c) => c.segments.map((i) => ({ z: c.z, s: i }))),
580
+ buses: corridors.flatMap((c) => c.buses.map((i) => ({ z: c.z, i })))
581
+ }));
582
+ return [
583
+ new LineLayer({
584
+ ...common,
585
+ id: 'zones',
586
+ data: segments,
587
+ getSourcePosition: (d) => [g.sources[d.s * 3], g.sources[d.s * 3 + 1], 0],
588
+ getTargetPosition: (d) => [g.targets[d.s * 3], g.targets[d.s * 3 + 1], 0],
589
+ getColor: (d) => band(d.z),
590
+ getWidth: CORRIDOR_PX,
591
+ widthUnits: 'pixels',
592
+ parameters: once,
593
+ getPolygonOffset: null,
594
+ pickable: true,
595
+ updateTriggers: { getColor: states }
596
+ }),
597
+ new ScatterplotLayer({
598
+ ...common,
599
+ id: 'zones-buses',
600
+ data: buses,
601
+ getPosition: (d) => [g.positions[d.i * 3], g.positions[d.i * 3 + 1], 0],
602
+ getFillColor: (d) => band(d.z),
603
+ getRadius: CORRIDOR_PX / 2,
604
+ radiusUnits: 'pixels',
605
+ parameters: once,
606
+ getPolygonOffset: null,
607
+ pickable: true,
608
+ updateTriggers: { getFillColor: states }
609
+ })
610
+ ];
611
+ }
612
+ /** The hovered network and feeder, as halos beneath the lines: the network faint, the feeder
613
+ * strong, the same on MV and LV. */
614
+ function hoverHalos(kit, s) {
615
+ const h = s.hover;
616
+ if (!h)
617
+ return [];
618
+ const d = s.drawn;
619
+ const halo = (id, edges, a) => new kit.layers.LineLayer({
620
+ ...cartesian(kit),
621
+ id,
622
+ data: highlighted(d, s.cache.segmentsOf([d], () => segmentIndex(d)), edges, s.fan),
623
+ getSourcePosition: (i) => [d.sources[i * 3], d.sources[i * 3 + 1], 0],
624
+ getTargetPosition: (i) => [d.targets[i * 3], d.targets[i * 3 + 1], 0],
625
+ getColor: alpha(h.tint, a),
626
+ getWidth: HALO_PX,
627
+ widthUnits: 'pixels'
628
+ });
629
+ return [halo('hover-network', h.network, 90), halo('hover-feeder', h.feeder, 190)];
630
+ }
631
+ /** Over the lines: the path back to the source, when shown, and a ring at the supply point. */
632
+ function hoverMarks(kit, s) {
633
+ const h = s.hover;
634
+ if (!h)
635
+ return [];
636
+ const d = s.drawn;
637
+ const common = cartesian(kit);
638
+ return [
639
+ ...(h.path.size
640
+ ? [
641
+ new kit.layers.LineLayer({
642
+ ...common,
643
+ id: 'hover-path',
644
+ data: highlighted(d, s.cache.segmentsOf([d], () => segmentIndex(d)), h.path, s.fan),
645
+ getSourcePosition: (i) => [d.sources[i * 3], d.sources[i * 3 + 1], 0],
646
+ getTargetPosition: (i) => [d.targets[i * 3], d.targets[i * 3 + 1], 0],
647
+ getColor: PATH,
648
+ getWidth: 3,
649
+ widthUnits: 'pixels'
650
+ })
651
+ ]
652
+ : []),
653
+ ...(h.supply
654
+ ? [
655
+ new kit.layers.ScatterplotLayer({
656
+ ...common,
657
+ id: 'hover-supply',
658
+ data: [h.supply],
659
+ getPosition: (p) => [...p, 0],
660
+ getFillColor: alpha(h.tint, 50),
661
+ getLineColor: alpha(h.tint, 220),
662
+ stroked: true,
663
+ getRadius: 13,
664
+ radiusUnits: 'pixels',
665
+ getLineWidth: 2,
666
+ lineWidthUnits: 'pixels'
667
+ })
668
+ ]
669
+ : [])
670
+ ];
671
+ }
672
+ /** Spokes from each spread stack's true point to its buses; a ring and ×n badge on the others. */
673
+ function stackLayers(kit, s) {
674
+ const stacks = s.stacks;
675
+ if (!stacks)
676
+ return [];
677
+ const { LineLayer, ScatterplotLayer, TextLayer } = kit.layers;
678
+ const common = cartesian(kit);
679
+ const base = s.shown;
680
+ const point = (g) => {
681
+ const i = stacks.groups[g][0];
682
+ return [base.positions[i * 3], base.positions[i * 3 + 1]];
683
+ };
684
+ const spokes = [];
685
+ for (const g of s.spreadStacks)
686
+ for (const i of stacks.groups[g])
687
+ spokes.push({
688
+ from: point(g),
689
+ to: [s.geometry.positions[i * 3], s.geometry.positions[i * 3 + 1]]
690
+ });
691
+ // A spread stack keeps a faint ring around its circle, so the gaps between its buses still
692
+ // belong to it: the hover holds and a click there pins it.
693
+ const rings = stacks.groups.flatMap((members, g) => s.spreadStacks.includes(g)
694
+ ? [{ g, n: members.length, position: point(g), spread: true }]
695
+ : s.symbols
696
+ ? [{ g, n: members.length, position: point(g), spread: false }]
697
+ : []);
698
+ const closed = rings.filter((r) => !r.spread);
699
+ return [
700
+ ...(spokes.length
701
+ ? [
702
+ new LineLayer({
703
+ ...common,
704
+ id: 'stack-spokes',
705
+ data: spokes,
706
+ getSourcePosition: (d) => [...d.from, 0],
707
+ getTargetPosition: (d) => [...d.to, 0],
708
+ getColor: [120, 114, 102, 140],
709
+ getWidth: 1,
710
+ widthUnits: 'pixels'
711
+ })
712
+ ]
713
+ : []),
714
+ ...(rings.length
715
+ ? [
716
+ new ScatterplotLayer({
717
+ ...common,
718
+ id: 'stack-rings',
719
+ data: rings,
720
+ getPosition: (d) => [...d.position, 0],
721
+ stroked: true,
722
+ filled: true,
723
+ getFillColor: [0, 0, 0, 1],
724
+ getLineColor: (d) => d.spread ? alpha(SYMBOL_INK, 70) : SYMBOL_INK,
725
+ getRadius: (d) => (d.spread ? stackRadius(d.n) + 6 : 6),
726
+ radiusUnits: 'pixels',
727
+ lineWidthUnits: 'pixels',
728
+ getLineWidth: 1,
729
+ pickable: true,
730
+ updateTriggers: { getLineColor: s.spreadStacks, getRadius: s.spreadStacks }
731
+ })
732
+ ]
733
+ : []),
734
+ ...(closed.length
735
+ ? [
736
+ new TextLayer({
737
+ ...common,
738
+ id: 'stack-badges',
739
+ data: closed,
740
+ getPosition: (d) => [...d.position, 0],
741
+ getText: (d) => `×${d.n}`,
742
+ getColor: SYMBOL_INK,
743
+ getSize: 10,
744
+ getTextAnchor: 'end',
745
+ getAlignmentBaseline: 'bottom',
746
+ getPixelOffset: [-6, -5],
747
+ background: true,
748
+ getBackgroundColor: paperText(220),
749
+ backgroundPadding: [2, 1],
750
+ fontFamily: MONO,
751
+ characterSet: 'auto'
752
+ })
753
+ ]
754
+ : [])
755
+ ];
756
+ }