@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.
- package/README.md +20 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +10 -1
- package/dist/TellegenViewer.svelte +2 -0
- package/dist/basemap.d.ts +14 -0
- package/dist/basemap.js +27 -0
- package/dist/colors.d.ts +6 -1
- package/dist/colors.js +11 -4
- package/dist/components/DemandSlider.svelte +0 -2
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/McResults.svelte +18 -3
- package/dist/components/McWorkbench.svelte +688 -0
- package/dist/components/McWorkbench.svelte.d.ts +38 -0
- package/dist/components/MultiWire.svelte +136 -0
- package/dist/components/MultiWire.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +7 -0
- package/dist/components/OmniSearch.svelte +163 -0
- package/dist/components/OmniSearch.svelte.d.ts +14 -0
- package/dist/components/ResultsTable.svelte +259 -0
- package/dist/components/ResultsTable.svelte.d.ts +13 -0
- package/dist/components/SchematicCanvas.svelte +1896 -0
- package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
- package/dist/components/SolveCard.svelte +6 -0
- package/dist/components/schematic/PickCard.svelte +126 -0
- package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
- package/dist/components/schematic/export.d.ts +21 -0
- package/dist/components/schematic/export.js +110 -0
- package/dist/components/schematic/layers.d.ts +161 -0
- package/dist/components/schematic/layers.js +756 -0
- package/dist/components/schematic/paint.d.ts +28 -0
- package/dist/components/schematic/paint.js +69 -0
- package/dist/components/schematic/palette.d.ts +34 -0
- package/dist/components/schematic/palette.js +54 -0
- package/dist/components/schematic/text.d.ts +83 -0
- package/dist/components/schematic/text.js +190 -0
- package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
- package/dist/components/schematic/view-prefs.svelte.js +77 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +1 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +33 -5
- package/dist/controller.svelte.js +173 -20
- package/dist/diagram/basemap.d.ts +28 -0
- package/dist/diagram/basemap.js +39 -0
- package/dist/diagram/bus-stacks.d.ts +19 -0
- package/dist/diagram/bus-stacks.js +99 -0
- package/dist/diagram/colour-modes.d.ts +18 -0
- package/dist/diagram/colour-modes.js +33 -0
- package/dist/diagram/connections.d.ts +30 -0
- package/dist/diagram/connections.js +129 -0
- package/dist/diagram/display-tree.d.ts +21 -0
- package/dist/diagram/display-tree.js +65 -0
- package/dist/diagram/energisation.d.ts +10 -0
- package/dist/diagram/energisation.js +31 -0
- package/dist/diagram/feeders.d.ts +30 -0
- package/dist/diagram/feeders.js +57 -0
- package/dist/diagram/galvanic-zones.d.ts +60 -0
- package/dist/diagram/galvanic-zones.js +210 -0
- package/dist/diagram/graph-inputs.d.ts +18 -0
- package/dist/diagram/graph-inputs.js +103 -0
- package/dist/diagram/index.d.ts +28 -0
- package/dist/diagram/index.js +28 -0
- package/dist/diagram/landmarks.d.ts +50 -0
- package/dist/diagram/landmarks.js +153 -0
- package/dist/diagram/layout-morph.d.ts +15 -0
- package/dist/diagram/layout-morph.js +32 -0
- package/dist/diagram/multi-wire.d.ts +44 -0
- package/dist/diagram/multi-wire.js +65 -0
- package/dist/diagram/phase-bars.d.ts +35 -0
- package/dist/diagram/phase-bars.js +44 -0
- package/dist/diagram/results-table.d.ts +47 -0
- package/dist/diagram/results-table.js +173 -0
- package/dist/diagram/schematic-equipment.d.ts +133 -0
- package/dist/diagram/schematic-equipment.js +645 -0
- package/dist/diagram/schematic-layout.d.ts +51 -0
- package/dist/diagram/schematic-layout.js +240 -0
- package/dist/diagram/search.d.ts +36 -0
- package/dist/diagram/search.js +48 -0
- package/dist/diagram/shared-routes.d.ts +21 -0
- package/dist/diagram/shared-routes.js +40 -0
- package/dist/diagram/short-names.d.ts +11 -0
- package/dist/diagram/short-names.js +54 -0
- package/dist/diagram/snapshot.d.ts +115 -0
- package/dist/diagram/snapshot.js +242 -0
- package/dist/diagram/summary.d.ts +65 -0
- package/dist/diagram/summary.js +144 -0
- package/dist/diagram/svg-export.d.ts +80 -0
- package/dist/diagram/svg-export.js +108 -0
- package/dist/diagram/tree-walk.d.ts +13 -0
- package/dist/diagram/tree-walk.js +83 -0
- package/dist/diagram/value-labels.d.ts +24 -0
- package/dist/diagram/value-labels.js +69 -0
- package/dist/diagram/view-state.d.ts +12 -0
- package/dist/diagram/view-state.js +22 -0
- package/dist/diagram/voltage-levels.d.ts +14 -0
- package/dist/diagram/voltage-levels.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/state.svelte.d.ts +37 -1
- package/dist/state.svelte.js +35 -0
- package/dist/transformer-icon.d.ts +22 -13
- package/dist/transformer-icon.js +23 -13
- 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;
|