@tellegen/svelte 0.4.2 → 0.4.4

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 (106) hide show
  1. package/README.md +20 -0
  2. package/dist/TellegenMap.svelte +63 -22
  3. package/dist/TellegenProvider.svelte +7 -4
  4. package/dist/TellegenShell.svelte +10 -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/DemandSlider.svelte +0 -2
  11. package/dist/components/FormulationSelector.svelte +1 -1
  12. package/dist/components/LocalCaseDetails.svelte +18 -3
  13. package/dist/components/McResults.svelte +18 -3
  14. package/dist/components/McWorkbench.svelte +688 -0
  15. package/dist/components/McWorkbench.svelte.d.ts +38 -0
  16. package/dist/components/MultiWire.svelte +136 -0
  17. package/dist/components/MultiWire.svelte.d.ts +7 -0
  18. package/dist/components/MulticonductorDetails.svelte +7 -0
  19. package/dist/components/OmniSearch.svelte +163 -0
  20. package/dist/components/OmniSearch.svelte.d.ts +14 -0
  21. package/dist/components/ResultsTable.svelte +259 -0
  22. package/dist/components/ResultsTable.svelte.d.ts +13 -0
  23. package/dist/components/SchematicCanvas.svelte +1896 -0
  24. package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
  25. package/dist/components/SolveCard.svelte +6 -0
  26. package/dist/components/schematic/PickCard.svelte +126 -0
  27. package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
  28. package/dist/components/schematic/export.d.ts +21 -0
  29. package/dist/components/schematic/export.js +110 -0
  30. package/dist/components/schematic/layers.d.ts +161 -0
  31. package/dist/components/schematic/layers.js +756 -0
  32. package/dist/components/schematic/paint.d.ts +28 -0
  33. package/dist/components/schematic/paint.js +69 -0
  34. package/dist/components/schematic/palette.d.ts +34 -0
  35. package/dist/components/schematic/palette.js +54 -0
  36. package/dist/components/schematic/text.d.ts +83 -0
  37. package/dist/components/schematic/text.js +190 -0
  38. package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
  39. package/dist/components/schematic/view-prefs.svelte.js +77 -0
  40. package/dist/components.d.ts +1 -0
  41. package/dist/components.js +1 -0
  42. package/dist/context.svelte.d.ts +2 -0
  43. package/dist/context.svelte.js +2 -0
  44. package/dist/controller.svelte.d.ts +33 -5
  45. package/dist/controller.svelte.js +173 -20
  46. package/dist/diagram/basemap.d.ts +28 -0
  47. package/dist/diagram/basemap.js +39 -0
  48. package/dist/diagram/bus-stacks.d.ts +19 -0
  49. package/dist/diagram/bus-stacks.js +99 -0
  50. package/dist/diagram/colour-modes.d.ts +18 -0
  51. package/dist/diagram/colour-modes.js +33 -0
  52. package/dist/diagram/connections.d.ts +30 -0
  53. package/dist/diagram/connections.js +129 -0
  54. package/dist/diagram/display-tree.d.ts +21 -0
  55. package/dist/diagram/display-tree.js +65 -0
  56. package/dist/diagram/energisation.d.ts +10 -0
  57. package/dist/diagram/energisation.js +31 -0
  58. package/dist/diagram/feeders.d.ts +30 -0
  59. package/dist/diagram/feeders.js +57 -0
  60. package/dist/diagram/galvanic-zones.d.ts +60 -0
  61. package/dist/diagram/galvanic-zones.js +210 -0
  62. package/dist/diagram/graph-inputs.d.ts +18 -0
  63. package/dist/diagram/graph-inputs.js +103 -0
  64. package/dist/diagram/index.d.ts +28 -0
  65. package/dist/diagram/index.js +28 -0
  66. package/dist/diagram/landmarks.d.ts +50 -0
  67. package/dist/diagram/landmarks.js +153 -0
  68. package/dist/diagram/layout-morph.d.ts +15 -0
  69. package/dist/diagram/layout-morph.js +32 -0
  70. package/dist/diagram/multi-wire.d.ts +44 -0
  71. package/dist/diagram/multi-wire.js +65 -0
  72. package/dist/diagram/phase-bars.d.ts +35 -0
  73. package/dist/diagram/phase-bars.js +44 -0
  74. package/dist/diagram/results-table.d.ts +47 -0
  75. package/dist/diagram/results-table.js +173 -0
  76. package/dist/diagram/schematic-equipment.d.ts +133 -0
  77. package/dist/diagram/schematic-equipment.js +645 -0
  78. package/dist/diagram/schematic-layout.d.ts +51 -0
  79. package/dist/diagram/schematic-layout.js +240 -0
  80. package/dist/diagram/search.d.ts +36 -0
  81. package/dist/diagram/search.js +48 -0
  82. package/dist/diagram/shared-routes.d.ts +21 -0
  83. package/dist/diagram/shared-routes.js +40 -0
  84. package/dist/diagram/short-names.d.ts +11 -0
  85. package/dist/diagram/short-names.js +54 -0
  86. package/dist/diagram/snapshot.d.ts +115 -0
  87. package/dist/diagram/snapshot.js +242 -0
  88. package/dist/diagram/summary.d.ts +65 -0
  89. package/dist/diagram/summary.js +144 -0
  90. package/dist/diagram/svg-export.d.ts +80 -0
  91. package/dist/diagram/svg-export.js +108 -0
  92. package/dist/diagram/tree-walk.d.ts +13 -0
  93. package/dist/diagram/tree-walk.js +83 -0
  94. package/dist/diagram/value-labels.d.ts +24 -0
  95. package/dist/diagram/value-labels.js +69 -0
  96. package/dist/diagram/view-state.d.ts +12 -0
  97. package/dist/diagram/view-state.js +22 -0
  98. package/dist/diagram/voltage-levels.d.ts +14 -0
  99. package/dist/diagram/voltage-levels.js +20 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/state.svelte.d.ts +37 -1
  103. package/dist/state.svelte.js +35 -0
  104. package/dist/transformer-icon.d.ts +22 -13
  105. package/dist/transformer-icon.js +23 -13
  106. package/package.json +2 -2
@@ -0,0 +1,645 @@
1
+ import { terminalConductors, transformerGlyph, transformerInfo } from './connections.js';
2
+ const circle = (cx, cy, r, from = 0, to = 360, steps = 20) => {
3
+ const out = [];
4
+ for (let i = 0; i <= steps; i++) {
5
+ const a = ((from + ((to - from) * i) / steps) * Math.PI) / 180;
6
+ out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
7
+ }
8
+ return out;
9
+ };
10
+ const sine = (cx, cy, half, amp, steps = 12) => Array.from({ length: steps + 1 }, (_, i) => {
11
+ const t = i / steps;
12
+ return [cx - half + 2 * half * t, cy - amp * Math.sin(2 * Math.PI * t)];
13
+ });
14
+ /** Branch glyphs, centred on the branch and spanning `±INLINE_HALF` along it, where the drawn line
15
+ * is interrupted. Shapes follow the #177 specimens: a two-winding transformer as two overlapping
16
+ * circles (no conductor continuity), a generic switch as a blade between two contacts, open when
17
+ * the pole is open. A line has no glyph. */
18
+ export const INLINE_HALF = 1.4;
19
+ export function branchGlyph(kind, closed) {
20
+ if (kind === 'transformer')
21
+ return [
22
+ [
23
+ [-1.4, 0],
24
+ [-1.15, 0]
25
+ ],
26
+ circle(-0.45, 0, 0.7),
27
+ circle(0.45, 0, 0.7),
28
+ [
29
+ [1.15, 0],
30
+ [1.4, 0]
31
+ ]
32
+ ];
33
+ if (kind === 'switch')
34
+ return [
35
+ [
36
+ [-1.4, 0],
37
+ [-1.05, 0]
38
+ ],
39
+ circle(-0.9, 0, 0.15, 0, 360, 8),
40
+ circle(0.9, 0, 0.15, 0, 360, 8),
41
+ [
42
+ [1.05, 0],
43
+ [1.4, 0]
44
+ ],
45
+ closed
46
+ ? [
47
+ [-0.9, 0],
48
+ [1.05, 0]
49
+ ]
50
+ : [
51
+ [-0.9, 0],
52
+ [0.7, -0.85]
53
+ ]
54
+ ];
55
+ return null;
56
+ }
57
+ /** Attachment glyphs, connected at the origin and extending along +x. A load is a demand arrow, a
58
+ * generator a machine circle with a G, an external source an AC supply circle with a sinusoid, an
59
+ * inverter-based resource a converter box (DC bar, diagonal, AC wave), and a shunt a general
60
+ * admittance block: BMOPF shunts can be conductive, so no capacitor is asserted. A shunt on a bus's
61
+ * neutral and nothing else is a neutral earth through an impedance (an MEN electrode, say): drawn
62
+ * as an impedance box in series with the IEC 60617 earth (three shrinking bars), not as a solid
63
+ * earth, since a shunt always has finite admittance. A perfectly grounded terminal is a solid earth:
64
+ * the earth symbol alone. */
65
+ export function attachmentGlyph(kind) {
66
+ const lead = [
67
+ [0, 0],
68
+ [0.4, 0]
69
+ ];
70
+ switch (kind) {
71
+ case 'load':
72
+ return [
73
+ [
74
+ [0, 0],
75
+ [0.6, 0]
76
+ ],
77
+ [
78
+ [0.6, -0.7],
79
+ [1.8, 0],
80
+ [0.6, 0.7],
81
+ [0.6, -0.7]
82
+ ]
83
+ ];
84
+ case 'generator':
85
+ return [lead, circle(1.3, 0, 0.9), [...circle(1.3, 0, 0.45, -45, -360, 14), [1.4, 0]]];
86
+ case 'source':
87
+ return [lead, circle(1.3, 0, 0.9), sine(1.3, 0, 0.5, 0.3)];
88
+ case 'ibr':
89
+ return [
90
+ lead,
91
+ [
92
+ [0.4, -0.9],
93
+ [2.2, -0.9],
94
+ [2.2, 0.9],
95
+ [0.4, 0.9],
96
+ [0.4, -0.9]
97
+ ],
98
+ [
99
+ [0.4, 0.9],
100
+ [2.2, -0.9]
101
+ ],
102
+ [
103
+ [0.6, -0.5],
104
+ [1.1, -0.5]
105
+ ],
106
+ sine(1.75, 0.5, 0.3, 0.18, 8)
107
+ ];
108
+ case 'ground':
109
+ return [
110
+ [
111
+ [0, 0],
112
+ [0.8, 0],
113
+ [0.8, 0.3]
114
+ ],
115
+ [
116
+ [0.4, 0.3],
117
+ [1.2, 0.3]
118
+ ],
119
+ [
120
+ [0.55, 0.55],
121
+ [1.05, 0.55]
122
+ ],
123
+ [
124
+ [0.7, 0.8],
125
+ [0.9, 0.8]
126
+ ]
127
+ ];
128
+ case 'earth':
129
+ return [
130
+ [
131
+ [0, 0],
132
+ [0.3, 0]
133
+ ],
134
+ [
135
+ [0.3, -0.22],
136
+ [1.1, -0.22],
137
+ [1.1, 0.22],
138
+ [0.3, 0.22],
139
+ [0.3, -0.22]
140
+ ],
141
+ [
142
+ [1.1, 0],
143
+ [1.4, 0],
144
+ [1.4, 0.3]
145
+ ],
146
+ [
147
+ [1.0, 0.3],
148
+ [1.8, 0.3]
149
+ ],
150
+ [
151
+ [1.15, 0.55],
152
+ [1.65, 0.55]
153
+ ],
154
+ [
155
+ [1.3, 0.8],
156
+ [1.5, 0.8]
157
+ ]
158
+ ];
159
+ case 'shunt':
160
+ return [
161
+ lead,
162
+ [
163
+ [0.4, -0.9],
164
+ [2.0, -0.9],
165
+ [2.0, 0.9],
166
+ [0.4, 0.9],
167
+ [0.4, -0.9]
168
+ ],
169
+ [
170
+ [0.8, -0.4],
171
+ [1.2, 0],
172
+ [0.8, 0.4]
173
+ ],
174
+ [
175
+ [1.2, 0],
176
+ [1.7, 0]
177
+ ]
178
+ ];
179
+ }
180
+ }
181
+ /** Conductor marks across a line, centred on it: one slash per phase conductor and a slash ending
182
+ * in a small circle for the neutral (the IEC 60617 convention). */
183
+ export function conductorTicks(phases, neutral) {
184
+ const n = phases + (neutral ? 1 : 0);
185
+ const out = [];
186
+ for (let i = 0; i < n; i++) {
187
+ const x = (i - (n - 1) / 2) * 0.4;
188
+ out.push([
189
+ [x - 0.25, 0.45],
190
+ [x + 0.25, -0.45]
191
+ ]);
192
+ if (neutral && i === n - 1)
193
+ out.push(circle(x + 0.33, -0.6, 0.13, 0, 360, 8));
194
+ }
195
+ return out;
196
+ }
197
+ /** Each graph's neutral terminal per bus id (lower case), built once: `conductorSet` runs per edge,
198
+ * and a scan of the buses each time made the equipment quadratic in the network size. */
199
+ const neutralsByGraph = new WeakMap();
200
+ function neutralOf(graph, bus) {
201
+ let index = neutralsByGraph.get(graph);
202
+ if (!index) {
203
+ index = new Map();
204
+ // The first bus of an id wins, as a search in graph order would find it.
205
+ for (const b of graph.buses) {
206
+ const id = b.id.toLowerCase();
207
+ if (!index.has(id))
208
+ index.set(id, b.neutral_terminal ?? null);
209
+ }
210
+ neutralsByGraph.set(graph, index);
211
+ }
212
+ return index.get(bus.toLowerCase()) ?? null;
213
+ }
214
+ /** Phase conductors and whether a neutral runs, from a branch's conductor pairs: a conductor whose
215
+ * from terminal is the from bus's neutral is the neutral. */
216
+ export function conductorSet(graph, k) {
217
+ const edge = graph.edges[k];
218
+ const n = neutralOf(graph, edge.from);
219
+ const neutral = n !== null && edge.conductors.some(([a]) => a === n);
220
+ return { phases: edge.conductors.length - (neutral ? 1 : 0), neutral };
221
+ }
222
+ /** Drawing order of attachment kinds below a bus. A neutral earth shares the shunt slot (the
223
+ * engine layout reserves one slot per PowerIO kind). */
224
+ export const ATTACHMENT_ORDER = [
225
+ 'source',
226
+ 'generator',
227
+ 'ibr',
228
+ 'load',
229
+ 'shunt',
230
+ 'earth',
231
+ 'ground'
232
+ ];
233
+ export const ATTACHMENT_NAMES = {
234
+ source: 'source',
235
+ generator: 'generator',
236
+ ibr: 'inverter-based resource',
237
+ load: 'load',
238
+ shunt: 'shunt',
239
+ earth: 'neutral earth',
240
+ ground: 'solid earth'
241
+ };
242
+ /** Shunts of `bus` connected to its neutral terminal and to nothing else: neutral earths. */
243
+ export function neutralEarths(bus) {
244
+ const neutral = bus.neutral_terminal;
245
+ const out = new Set();
246
+ if (!neutral)
247
+ return out;
248
+ const elsewhere = new Set();
249
+ for (const [terminal, list] of Object.entries(bus.terminal_attachments ?? {}))
250
+ for (const a of list)
251
+ if (a.kind === 'shunt')
252
+ (terminal === neutral ? out : elsewhere).add(a.id);
253
+ for (const id of elsewhere)
254
+ out.delete(id);
255
+ return out;
256
+ }
257
+ /** A bus's attachments by kind, split by how they connect. Without per-attachment detail (a graph
258
+ * from before connections were exported) each kind is one variant without terminals. */
259
+ function attachmentVariants(bus, earths) {
260
+ const out = new Map();
261
+ const symbol = (kind, id) => kind === 'shunt' && earths.has(id) ? 'earth' : kind;
262
+ if (bus.attachments?.length) {
263
+ const keyed = new Map();
264
+ for (const a of bus.attachments) {
265
+ const kind = symbol(a.kind, a.id);
266
+ const key = `${kind}|${a.connection ?? ''}|${a.terminals.join(',')}`;
267
+ let v = keyed.get(key);
268
+ if (!v) {
269
+ v = { ids: [], terminals: a.terminals, connection: a.connection };
270
+ keyed.set(key, v);
271
+ out.set(kind, [...(out.get(kind) ?? []), v]);
272
+ }
273
+ v.ids.push(a.id);
274
+ }
275
+ return out;
276
+ }
277
+ for (const list of Object.values(bus.terminal_attachments ?? {}))
278
+ for (const a of list) {
279
+ const kind = symbol(a.kind, a.id);
280
+ const v = out.get(kind)?.[0] ?? { ids: [], terminals: null };
281
+ if (!v.ids.includes(a.id))
282
+ v.ids.push(a.id);
283
+ out.set(kind, [v]);
284
+ }
285
+ return out;
286
+ }
287
+ /** Room along a slot for one attachment variant: the lead with its conductor marks, the glyph, and
288
+ * its label. */
289
+ const LEAD = 1.6;
290
+ const VARIANT_STEP = 7.2;
291
+ function medianEdgeLength(geometry) {
292
+ const lengths = [];
293
+ for (let s = 0; s < geometry.segmentEdge.length; s++)
294
+ lengths.push(Math.hypot(geometry.targets[s * 3] - geometry.sources[s * 3], geometry.targets[s * 3 + 1] - geometry.sources[s * 3 + 1]));
295
+ const positive = lengths.filter((l) => l > 0).sort((a, b) => a - b);
296
+ return positive.length ? positive[positive.length >> 1] : 1;
297
+ }
298
+ /** Symbols for every branch and attachment of `graph` drawn on `geometry`.
299
+ *
300
+ * Inline symbols sit on the child-end segment of a tree route, a fixed distance from the child bus
301
+ * so they line up in columns; other branches carry theirs at the middle of their middle segment.
302
+ * Attachments hang from a riser below the bus, one slot per kind (the engine layout reserves that
303
+ * room), each on a short tick with its count when there are several. */
304
+ export function schematicEquipment(graph, geometry) {
305
+ const engine = geometry.attachmentGap !== undefined && geometry.attachmentGap > 0;
306
+ const gap = engine ? geometry.attachmentGap : medianEdgeLength(geometry) * 0.25;
307
+ const unit = gap * 0.4;
308
+ const src = [];
309
+ const dst = [];
310
+ const own = [];
311
+ const tick = { src: [], dst: [], edge: [] };
312
+ const items = [];
313
+ const kinds = new Set();
314
+ const anchors = new Float32Array(graph.edges.length * 2).fill(NaN);
315
+ const flowDir = new Float32Array(graph.edges.length * 2);
316
+ const room = new Float32Array(graph.edges.length);
317
+ const busIndex = new Map(graph.buses.map((b, i) => [b.id.toLowerCase(), i]));
318
+ const stroke = (glyph, x, y, cos, sin, owner, out = { src, dst, edge: own }) => {
319
+ for (const line of glyph)
320
+ for (let i = 1; i < line.length; i++) {
321
+ const [ax, ay] = line[i - 1];
322
+ const [bx, by] = line[i];
323
+ out.src.push(x + unit * (ax * cos - ay * sin), y + unit * (ax * sin + ay * cos), 0);
324
+ out.dst.push(x + unit * (bx * cos - by * sin), y + unit * (bx * sin + by * cos), 0);
325
+ out.edge.push(owner);
326
+ }
327
+ };
328
+ // Segments per edge, in order.
329
+ const segs = new Map();
330
+ for (let s = 0; s < geometry.segmentEdge.length; s++) {
331
+ const k = geometry.segmentEdge[s];
332
+ const list = segs.get(k);
333
+ if (list)
334
+ list.push(s);
335
+ else
336
+ segs.set(k, [s]);
337
+ }
338
+ /** Per host segment: the interrupted interval as fractions along it. */
339
+ const cuts = new Map();
340
+ const half = INLINE_HALF * unit;
341
+ // The segment each branch's symbol or marks sit on, and which branches share one exactly:
342
+ // their marks are spread along it instead of drawn on top of each other.
343
+ const host = new Int32Array(graph.edges.length).fill(-1);
344
+ const routes = new Map();
345
+ // A multi-winding transformer projects to one edge per winding pair; it is drawn once, on its
346
+ // first edge, and its other edges stay plain lines.
347
+ const seenTransformers = new Set();
348
+ const secondary = graph.edges.map((e) => {
349
+ if (e.kind !== 'transformer')
350
+ return false;
351
+ if (seenTransformers.has(e.id))
352
+ return true;
353
+ seenTransformers.add(e.id);
354
+ return false;
355
+ });
356
+ graph.edges.forEach((_, k) => {
357
+ const list = segs.get(k);
358
+ if (!list || secondary[k])
359
+ return;
360
+ const s = engine && geometry.role[k] === 0 ? list[list.length - 1] : list[list.length >> 1];
361
+ host[k] = s;
362
+ const a = `${geometry.sources[s * 3]},${geometry.sources[s * 3 + 1]}`;
363
+ const b = `${geometry.targets[s * 3]},${geometry.targets[s * 3 + 1]}`;
364
+ const key = a < b ? `${a}|${b}` : `${b}|${a}`;
365
+ const group = routes.get(key);
366
+ if (group)
367
+ group.push(k);
368
+ else
369
+ routes.set(key, [k]);
370
+ });
371
+ const sharedRoute = new Uint8Array(graph.edges.length).fill(1);
372
+ const slot = new Int32Array(graph.edges.length);
373
+ const routeGroups = [];
374
+ const routeOf = new Int32Array(graph.edges.length).fill(-1);
375
+ for (const group of routes.values()) {
376
+ group.forEach((k, i) => {
377
+ sharedRoute[k] = Math.min(255, group.length);
378
+ slot[k] = i;
379
+ });
380
+ if (group.length > 1) {
381
+ for (const k of group)
382
+ routeOf[k] = routeGroups.length;
383
+ routeGroups.push(group);
384
+ }
385
+ }
386
+ graph.edges.forEach((edge, k) => {
387
+ const list = segs.get(k);
388
+ if (!list || secondary[k])
389
+ return;
390
+ const info = edge.kind === 'transformer' ? transformerInfo(graph, k) : null;
391
+ const glyph = edge.kind === 'transformer'
392
+ ? transformerGlyph(info, edge.n_phases)
393
+ : branchGlyph(edge.kind, edge.closed);
394
+ const childEnd = engine && geometry.role[k] === 0;
395
+ const s = host[k];
396
+ const [x0, y0, x1, y1] = [
397
+ geometry.sources[s * 3],
398
+ geometry.sources[s * 3 + 1],
399
+ geometry.targets[s * 3],
400
+ geometry.targets[s * 3 + 1]
401
+ ];
402
+ const len = Math.hypot(x1 - x0, y1 - y0);
403
+ // Co-located ends (a transformer between buses at one site): draw along x.
404
+ const cos = len > 0 ? (x1 - x0) / len : 1;
405
+ const sin = len > 0 ? (y1 - y0) / len : 0;
406
+ // Routes run either way between the ends; orient by which end the route starts nearer.
407
+ const from = busIndex.get(edge.from.toLowerCase()) ?? -1;
408
+ const to = busIndex.get(edge.to.toLowerCase()) ?? -1;
409
+ const first = list[0];
410
+ const near = (i) => i < 0
411
+ ? Infinity
412
+ : Math.hypot(geometry.sources[first * 3] - geometry.positions[i * 3], geometry.sources[first * 3 + 1] - geometry.positions[i * 3 + 1]);
413
+ const forward = near(from) <= near(to) ? 1 : -1;
414
+ flowDir[k * 2] = cos * forward;
415
+ flowDir[k * 2 + 1] = sin * forward;
416
+ if (len < 2.4 * half) {
417
+ // Too short to sit on: anchor at the middle. Transformers and switches still get their
418
+ // symbol there, over the branch rather than in a gap, so they stay visible.
419
+ const [mx, my] = [(x0 + x1) / 2, (y0 + y1) / 2];
420
+ anchors[k * 2] = mx;
421
+ anchors[k * 2 + 1] = my;
422
+ room[k] = len / 2;
423
+ if (glyph) {
424
+ stroke(glyph, mx, my, cos * forward, sin * forward, k);
425
+ const kind = edge.kind;
426
+ items.push({ kind, edge: k, bus: -1, ids: [edge.id], x: mx, y: my, label: info?.label });
427
+ kinds.add(kind);
428
+ }
429
+ return;
430
+ }
431
+ // From the child end: centre 2.5 half-widths back, clamped to the segment's middle. Branches
432
+ // sharing the segment take evenly spaced places along it.
433
+ const along = childEnd
434
+ ? Math.max(len / 2, len - 2.5 * half)
435
+ : Math.min(len - half, Math.max(half, (len * (slot[k] + 1)) / (sharedRoute[k] + 1)));
436
+ const x = x0 + cos * along;
437
+ const y = y0 + sin * along;
438
+ anchors[k * 2] = x;
439
+ anchors[k * 2 + 1] = y;
440
+ room[k] = along - half;
441
+ if (!glyph) {
442
+ const { phases, neutral } = conductorSet(graph, k);
443
+ if (phases + (neutral ? 1 : 0) > 0)
444
+ stroke(conductorTicks(phases, neutral), x, y, cos, sin, k, tick);
445
+ return;
446
+ }
447
+ // The from winding at −x, so a transformer's winding marks sit at their own buses.
448
+ stroke(glyph, x, y, cos * forward, sin * forward, k);
449
+ cuts.set(s, [(along - half) / len, (along + half) / len]);
450
+ items.push({
451
+ kind: edge.kind,
452
+ edge: k,
453
+ bus: -1,
454
+ ids: [edge.id],
455
+ x,
456
+ y,
457
+ label: info?.label
458
+ });
459
+ kinds.add(edge.kind);
460
+ });
461
+ graph.buses.forEach((bus, i) => {
462
+ const variants = attachmentVariants(bus, neutralEarths(bus));
463
+ // Perfectly grounded terminals: one solid earth, listing the terminals.
464
+ if (bus.grounded?.length)
465
+ variants.set('ground', [{ ids: [...bus.grounded], terminals: null }]);
466
+ if (!variants.size)
467
+ return;
468
+ const [bx, by] = [geometry.positions[i * 3], geometry.positions[i * 3 + 1]];
469
+ const present = ATTACHMENT_ORDER.filter((kind) => variants.has(kind));
470
+ // A neutral earth takes the shunt slot; with a real shunt too, it sits beside it.
471
+ const slots = present.filter((k) => !(k === 'earth' && variants.has('shunt')));
472
+ const owner = -(i + 1);
473
+ const line = (ax, ay, cx, cy) => {
474
+ src.push(ax, ay, 0);
475
+ dst.push(cx, cy, 0);
476
+ own.push(owner);
477
+ };
478
+ // Riser from the bus down to the last slot.
479
+ line(bx, by, bx, by + slots.length * gap);
480
+ present.forEach((kind) => {
481
+ const beside = kind === 'earth' && variants.has('shunt');
482
+ const slot = slots.indexOf(beside ? 'shunt' : kind);
483
+ const y = by + (slot + 1) * gap;
484
+ const list = variants.get(kind);
485
+ // Several variants in one slot hang from a rail above it, each on its own drop, so they do
486
+ // not read as in series.
487
+ const rail = y - 1.15 * unit;
488
+ if (list.length > 1 && !beside)
489
+ line(bx, rail, bx + unit * (list.length - 1) * VARIANT_STEP, rail);
490
+ // Beside a shunt, a neutral earth continues from the end of the last shunt's glyph, past
491
+ // room for that shunt's label.
492
+ const after = ((variants.get('shunt')?.length ?? 1) - 1) * VARIANT_STEP + LEAD + 2.0;
493
+ const labelRoom = 4.2;
494
+ list.forEach((v, j) => {
495
+ const drop = bx + unit * (beside ? after : j * VARIANT_STEP);
496
+ if (j > 0 && !beside)
497
+ line(drop, rail, drop, y);
498
+ const x0 = drop + unit * (beside ? labelRoom : LEAD);
499
+ line(drop, y, x0, y);
500
+ if (v.terminals && !beside) {
501
+ const { phases, neutral } = terminalConductors(v.terminals, bus.neutral_terminal);
502
+ if (phases + (neutral ? 1 : 0) > 0)
503
+ stroke(conductorTicks(phases, neutral), drop + (unit * LEAD) / 2, y, 1, 0, owner, tick);
504
+ }
505
+ stroke(attachmentGlyph(kind), x0, y, 1, 0, owner);
506
+ items.push({
507
+ kind,
508
+ edge: -1,
509
+ bus: i,
510
+ ids: v.ids,
511
+ x: x0 + unit * 1.2,
512
+ y,
513
+ ...(v.terminals ? { terminals: v.terminals, connection: v.connection } : {})
514
+ });
515
+ });
516
+ kinds.add(kind);
517
+ });
518
+ });
519
+ return {
520
+ sources: Float32Array.from(src),
521
+ targets: Float32Array.from(dst),
522
+ items,
523
+ cut: cuts.size ? interrupt(geometry, cuts) : geometry,
524
+ unit,
525
+ kinds,
526
+ owner: Int32Array.from(own),
527
+ ticks: {
528
+ sources: Float32Array.from(tick.src),
529
+ targets: Float32Array.from(tick.dst),
530
+ edge: Int32Array.from(tick.edge)
531
+ },
532
+ anchors,
533
+ flowDir,
534
+ room,
535
+ sharedRoute,
536
+ routeGroups,
537
+ routeOf,
538
+ host
539
+ };
540
+ }
541
+ /** A copy of `geometry` with each cut segment split around its interval; edge ownership is kept,
542
+ * so picking either half selects the branch. */
543
+ function interrupt(geometry, cuts) {
544
+ const m = geometry.segmentEdge.length + cuts.size;
545
+ const sources = new Float32Array(m * 3);
546
+ const targets = new Float32Array(m * 3);
547
+ const segmentEdge = new Int32Array(m);
548
+ let o = 0;
549
+ const push = (k, a, b) => {
550
+ sources.set(a, o * 3);
551
+ targets.set(b, o * 3);
552
+ segmentEdge[o++] = k;
553
+ };
554
+ for (let s = 0; s < geometry.segmentEdge.length; s++) {
555
+ const a = [geometry.sources[s * 3], geometry.sources[s * 3 + 1], 0];
556
+ const b = [geometry.targets[s * 3], geometry.targets[s * 3 + 1], 0];
557
+ const cut = cuts.get(s);
558
+ const k = geometry.segmentEdge[s];
559
+ if (!cut) {
560
+ push(k, a, b);
561
+ continue;
562
+ }
563
+ const at = (t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, 0];
564
+ push(k, a, at(cut[0]));
565
+ push(k, at(cut[1]), b);
566
+ }
567
+ return { ...geometry, sources, targets, segmentEdge };
568
+ }
569
+ /** An SVG path for a glyph in symbol units, for legends. */
570
+ export function glyphPath(glyph, scale, dx = 0, dy = 0) {
571
+ const f = (v) => Math.round(v * 100) / 100;
572
+ return glyph
573
+ .map((line) => line.map(([x, y], i) => `${i ? 'L' : 'M'}${f(dx + x * scale)} ${f(dy + y * scale)}`).join(''))
574
+ .join(' ');
575
+ }
576
+ /** Power-flow chevrons: one per closed branch with a result, just upstream of its symbol or marks
577
+ * on the same segment, pointing the way active power flows (from the `from` end when the power
578
+ * into the branch there is positive). Branches carrying under `minPower` (in the units of `p`)
579
+ * get none. */
580
+ export function flowArrows(symbols, ends, closed, minPower = 1) {
581
+ const src = [];
582
+ const dst = [];
583
+ const owner = [];
584
+ const u = symbols.unit;
585
+ const chevron = [
586
+ [-0.3, -0.4],
587
+ [0.3, 0],
588
+ [-0.3, 0.4]
589
+ ];
590
+ ends.forEach(([a, b], k) => {
591
+ const ax = symbols.anchors[k * 2];
592
+ if (!closed[k] || !Number.isFinite(ax))
593
+ return;
594
+ const p = a ? a.p : b ? -b.p : null;
595
+ if (p === null || !(Math.abs(p) >= minPower))
596
+ return;
597
+ const sign = p > 0 ? 1 : -1;
598
+ const cos = symbols.flowDir[k * 2] * sign;
599
+ const sin = symbols.flowDir[k * 2 + 1] * sign;
600
+ // Upstream along the segment's own direction, whichever way the power goes.
601
+ const back = Math.min(2 * INLINE_HALF * u, symbols.room[k] * 0.8);
602
+ const x = ax - symbols.flowDir[k * 2] * back;
603
+ const y = symbols.anchors[k * 2 + 1] - symbols.flowDir[k * 2 + 1] * back;
604
+ for (let i = 1; i < chevron.length; i++) {
605
+ const [px, py] = chevron[i - 1];
606
+ const [qx, qy] = chevron[i];
607
+ src.push(x + u * (px * cos - py * sin), y + u * (px * sin + py * cos), 0);
608
+ dst.push(x + u * (qx * cos - qy * sin), y + u * (qx * sin + qy * cos), 0);
609
+ owner.push(k);
610
+ }
611
+ });
612
+ return {
613
+ sources: Float32Array.from(src),
614
+ targets: Float32Array.from(dst),
615
+ edge: Int32Array.from(owner)
616
+ };
617
+ }
618
+ /** Port kinds of branches, which are not attachments. */
619
+ const BRANCH_PORT_KINDS = new Set(['line', 'switch', 'transformer']);
620
+ /** Complex power into each element (loads, generators, IBRs, shunts) by element id, and for
621
+ * sources the power they draw (the negative of what they inject), so every attachment reads with
622
+ * the same convention: positive is consumption. Branch ports are left out: names are unique only
623
+ * within a family, and a line sharing a load's name must not add to it. */
624
+ export function elementPowers(result) {
625
+ const out = new Map();
626
+ const add = (id, p, q) => {
627
+ const row = out.get(id) ?? { p: 0, q: 0 };
628
+ row.p += p;
629
+ row.q += q;
630
+ out.set(id, row);
631
+ };
632
+ for (const port of result.element_ports)
633
+ if (port.kind === undefined || !BRANCH_PORT_KINDS.has(port.kind))
634
+ add(port.element, port.power_into_element.re, port.power_into_element.im);
635
+ for (const r of result.source_reactions)
636
+ add(r.source, -r.power_into_network.re, -r.power_into_network.im);
637
+ return out;
638
+ }
639
+ /** `12.3 kW 4.1 kvar` for a power in W and var. */
640
+ export function formatPower(p, q) {
641
+ const scale = Math.max(Math.abs(p), Math.abs(q));
642
+ const [k, unit] = scale >= 1e6 ? [1e6, 'M'] : scale >= 1e3 ? [1e3, 'k'] : [1, ''];
643
+ const f = (v) => (v / k).toFixed(Math.abs(v / k) >= 100 ? 0 : 1);
644
+ return `${f(p)} ${unit}W ${f(q)} ${unit}var`;
645
+ }
@@ -0,0 +1,51 @@
1
+ import type { DistGraph, SchematicLayout } from '@tellegen/engine';
2
+ import type { DisplayTree } from './display-tree.js';
3
+ /** Drawing geometry for one layout: bus positions and branch segments (several per routed edge). */
4
+ export interface SchematicGeometry {
5
+ /** `x, y, 0` per bus in graph order; y grows downwards. */
6
+ positions: Float32Array;
7
+ sources: Float32Array;
8
+ targets: Float32Array;
9
+ /** Edge index (graph order) for each segment. */
10
+ segmentEdge: Int32Array;
11
+ /** Per edge: 0 tree route, 1 closed non-tree link, 2 open or unreachable. */
12
+ role: Uint8Array;
13
+ bounds: [number, number, number, number];
14
+ /** Engine layout version, for provenance; absent for source positions. */
15
+ version?: number;
16
+ /** Room reserved below each bus per attachment kind (engine layouts only). */
17
+ attachmentGap?: number;
18
+ /** For source positions: buses the data gives no position, drawn next to a connected bus
19
+ * instead (1), or left at the origin because nothing placed connects to them (2). */
20
+ inferred?: Uint8Array;
21
+ /** For geographic source positions: the Web Mercator point (metres, y down) the positions are
22
+ * relative to, so map tiles can be placed under them. */
23
+ mercatorOrigin?: [number, number];
24
+ }
25
+ /** Engine schematic layout (`mcSchematicLayout`, computed in Rust) as GPU-ready arrays. */
26
+ export declare function fromEngineLayout(layout: SchematicLayout): SchematicGeometry;
27
+ /** An empty drawing, before the first layout arrives. */
28
+ export declare const EMPTY_GEOMETRY: SchematicGeometry;
29
+ /** Positions as placed in the source data: geographic longitude/latitude projected to Web Mercator
30
+ * metres (y down), or drawing coordinates as given (already y down, as `DiagramCanvas` draws them),
31
+ * relative to their centroid. Branches are drawn straight. */
32
+ export declare function sourceLayout(graph: DistGraph, placed: {
33
+ id: string;
34
+ lon: number;
35
+ lat: number;
36
+ }[], geographic: boolean, tree: DisplayTree,
37
+ /** Routed paths from the case (`MultiView.edges`), drawn in place of straight segments. */
38
+ routes?: {
39
+ id: string;
40
+ from: string;
41
+ to: string;
42
+ path: [number, number][];
43
+ }[]): SchematicGeometry;
44
+ /** The drawn schematic as a PowerIO diagram layer (GeoJSON with `powerio_geo.space = "diagram"`):
45
+ * one point per bus and one route per line, matched by line id, y down. `kind: "synthetic"` marks
46
+ * the positions as computed, and `tellegen_layout` records how. Load it with the case to reuse the
47
+ * layout, or hand it to PowerIO tooling. */
48
+ export declare function diagramLayer(graph: DistGraph, geometry: SchematicGeometry, provenance: {
49
+ strategy: string;
50
+ version: number;
51
+ }): string;