@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,210 @@
1
+ import { busState, loadingState } from './summary.js';
2
+ /** Connected components over closed non-transformer branches. */
3
+ export function galvanicZones(graph, tree) {
4
+ const n = graph.buses.length;
5
+ const index = new Map(graph.buses.map((b, i) => [b.id.toLowerCase(), i]));
6
+ const ends = graph.edges.map((e) => [index.get(e.from.toLowerCase()) ?? -1, index.get(e.to.toLowerCase()) ?? -1]);
7
+ const root = Int32Array.from({ length: n }, (_, i) => i);
8
+ const find = (i) => {
9
+ while (root[i] !== i)
10
+ i = root[i] = root[root[i]];
11
+ return i;
12
+ };
13
+ graph.edges.forEach((e, k) => {
14
+ const [a, b] = ends[k];
15
+ if (a < 0 || b < 0 || !e.closed || e.kind === 'transformer')
16
+ return;
17
+ const ra = find(a);
18
+ const rb = find(b);
19
+ if (ra !== rb)
20
+ root[ra] = rb;
21
+ });
22
+ const busZone = new Int32Array(n).fill(-1);
23
+ const zones = [];
24
+ for (let i = 0; i < n; i++) {
25
+ const r = find(i);
26
+ if (busZone[r] < 0) {
27
+ busZone[r] = zones.length;
28
+ zones.push({ buses: [], edges: [], transformers: [], hasSource: false });
29
+ }
30
+ busZone[i] = busZone[r];
31
+ const z = zones[busZone[i]];
32
+ z.buses.push(i);
33
+ if (graph.buses[i].has_source)
34
+ z.hasSource = true;
35
+ }
36
+ graph.edges.forEach((e, k) => {
37
+ const [a, b] = ends[k];
38
+ if (a < 0 || b < 0)
39
+ return;
40
+ if (e.kind === 'transformer') {
41
+ for (const z of new Set([busZone[a], busZone[b]]))
42
+ zones[z].transformers.push(k);
43
+ }
44
+ else if (busZone[a] === busZone[b])
45
+ zones[busZone[a]].edges.push(k);
46
+ });
47
+ // The feeding transformer is the one the display tree enters the zone through.
48
+ if (tree)
49
+ for (const z of zones) {
50
+ const feed = z.buses
51
+ .map((i) => tree.parentEdge[i])
52
+ .find((k) => k >= 0 && graph.edges[k].kind === 'transformer');
53
+ if (feed !== undefined)
54
+ z.transformers = [feed, ...z.transformers.filter((k) => k !== feed)];
55
+ }
56
+ return { zones, busZone };
57
+ }
58
+ /** A zone's worst state: a voltage outside its band or a branch over its rating is a violation,
59
+ * loading at or above the warning threshold a warning. Its transformers count. */
60
+ export function zoneState(zone, snapshot) {
61
+ if (!snapshot)
62
+ return 'missing';
63
+ const rank = { missing: 0, normal: 1, warning: 2, violation: 3 };
64
+ let state = 'missing';
65
+ const raise = (s) => {
66
+ if (rank[s] > rank[state])
67
+ state = s;
68
+ };
69
+ for (const i of zone.buses) {
70
+ const s = busState(snapshot.buses[i]);
71
+ raise(s === 'violation' ? 'violation' : s === 'normal' ? 'normal' : 'missing');
72
+ }
73
+ for (const k of [...zone.transformers, ...zone.edges]) {
74
+ const s = loadingState(snapshot.branches[k]?.loading ?? null);
75
+ if (s !== 'missing')
76
+ raise(s);
77
+ }
78
+ return state;
79
+ }
80
+ /** The outline of a zone in a schematic layout: one band per drawing column (depth), from the
81
+ * zone's highest bus to the lowest attachment symbol below it, joined into a staircase. Tidy and
82
+ * leaf-order layouts keep each subtree's rows contiguous per column, so neighbouring zones'
83
+ * outlines do not overlap; a zone nested inside another sits inside its outline. */
84
+ export function zoneOutline(zone, positions, below, options) {
85
+ const columns = new Map();
86
+ for (const i of zone.buses) {
87
+ const x = positions[i * 3];
88
+ const y = positions[i * 3 + 1];
89
+ const c = columns.get(x);
90
+ if (c) {
91
+ c[0] = Math.min(c[0], y);
92
+ c[1] = Math.max(c[1], y + below[i]);
93
+ }
94
+ else
95
+ columns.set(x, [y, y + below[i]]);
96
+ }
97
+ const xs = [...columns.keys()].sort((a, b) => a - b);
98
+ const { dx, pad } = options;
99
+ // The first column starts just after the feeding transformer's symbol; the last leaves room
100
+ // for attachment symbols and their values to the right of the bus.
101
+ const left = (j) => (j === 0 ? xs[0] - 0.15 * dx : (xs[j - 1] + xs[j]) / 2);
102
+ const right = (j) => (j === xs.length - 1 ? xs[j] + 0.4 * dx : (xs[j] + xs[j + 1]) / 2);
103
+ const top = [];
104
+ const bottom = [];
105
+ xs.forEach((x, j) => {
106
+ const [lo, hi] = columns.get(x);
107
+ top.push([left(j), lo - pad], [right(j), lo - pad]);
108
+ bottom.push([left(j), hi + pad], [right(j), hi + pad]);
109
+ });
110
+ return [...top, ...bottom.reverse()];
111
+ }
112
+ export function zoneCorridors(zones, geometry, include) {
113
+ const edgeZone = new Map();
114
+ zones.zones.forEach((zone, z) => {
115
+ if (include(zone))
116
+ for (const k of zone.edges)
117
+ edgeZone.set(k, z);
118
+ });
119
+ const out = new Map();
120
+ zones.zones.forEach((zone, z) => {
121
+ if (!include(zone))
122
+ return;
123
+ let [x0, y0, x1, y1] = [Infinity, Infinity, -Infinity, -Infinity];
124
+ for (const i of zone.buses) {
125
+ const x = geometry.positions[i * 3];
126
+ const y = geometry.positions[i * 3 + 1];
127
+ [x0, y0, x1, y1] = [Math.min(x0, x), Math.min(y0, y), Math.max(x1, x), Math.max(y1, y)];
128
+ }
129
+ out.set(z, { z, segments: [], buses: zone.buses, bounds: [x0, y0, x1, y1] });
130
+ });
131
+ for (let s = 0; s < geometry.segmentEdge.length; s++) {
132
+ const z = edgeZone.get(geometry.segmentEdge[s]);
133
+ if (z !== undefined)
134
+ out.get(z).segments.push(s);
135
+ }
136
+ return [...out.values()];
137
+ }
138
+ /** A zone's voltage level in volts: the declared rating of a winding or source in it (the
139
+ * largest), else its declared limits (midpoint of the band; a phase–neutral or phase–ground band
140
+ * times √3), else its solved voltages, as the median over its buses. Null when none is known. */
141
+ export function zoneVoltage(zone, graph, snapshot) {
142
+ let nominal = 0;
143
+ for (const i of zone.buses)
144
+ nominal = Math.max(nominal, graph.buses[i].nominal_voltage ?? 0);
145
+ if (nominal > 0)
146
+ return nominal;
147
+ const levels = [];
148
+ const mid = (lo, hi) => lo?.length && hi?.length ? (Math.min(...lo) + Math.max(...hi)) / 2 : null;
149
+ const values = (byTerminal) => byTerminal && Object.values(byTerminal);
150
+ for (const i of zone.buses) {
151
+ const l = graph.buses[i].voltage_limits;
152
+ const pp = mid(l?.vpp_min, l?.vpp_max);
153
+ // Phase–neutral or phase–ground: the phase voltage, √3 below line to line.
154
+ const phaseBand = mid(l?.vpn_min, l?.vpn_max) ??
155
+ mid(values(l?.v_min_by_terminal), values(l?.v_max_by_terminal));
156
+ let v = pp ?? (phaseBand === null ? null : phaseBand * Math.sqrt(3));
157
+ if (v === null && snapshot) {
158
+ const pairs = snapshot.buses[i]?.pairs ?? [];
159
+ const phase = pairs.filter((p) => p.zone === 'phase–phase');
160
+ const single = pairs.filter((p) => p.zone !== 'phase–phase');
161
+ if (phase.length)
162
+ v = Math.max(...phase.map((p) => p.volts));
163
+ else if (single.length)
164
+ v = Math.max(...single.map((p) => p.volts)) * Math.sqrt(3);
165
+ }
166
+ if (v !== null && Number.isFinite(v) && v > 0)
167
+ levels.push(v);
168
+ }
169
+ if (!levels.length)
170
+ return null;
171
+ levels.sort((a, b) => a - b);
172
+ return levels[levels.length >> 1];
173
+ }
174
+ /** `mv` from 1 kV phase–phase up (drawn heavier), else `lv`. Without a known voltage the source
175
+ * zone counts as `mv`, others as `lv`. */
176
+ export function zoneLevel(zone, volts) {
177
+ if (volts === null)
178
+ return zone.hasSource ? 'mv' : 'lv';
179
+ return volts >= 1000 ? 'mv' : 'lv';
180
+ }
181
+ /** One-line zone summary for its label: lowest phase–neutral (else phase–ground, else
182
+ * phase–phase) voltage and highest loading among its branches and transformers, when solved. */
183
+ export function zoneSummary(zone, snapshot) {
184
+ if (!snapshot)
185
+ return { minVolts: null, maxLoading: null };
186
+ let pn = Infinity;
187
+ let pg = Infinity;
188
+ let pp = Infinity;
189
+ for (const i of zone.buses)
190
+ for (const p of snapshot.buses[i]?.pairs ?? []) {
191
+ if (!Number.isFinite(p.volts))
192
+ continue;
193
+ if (p.zone === 'phase–neutral')
194
+ pn = Math.min(pn, p.volts);
195
+ else if (p.zone === 'phase–ground')
196
+ pg = Math.min(pg, p.volts);
197
+ else
198
+ pp = Math.min(pp, p.volts);
199
+ }
200
+ let loading = -Infinity;
201
+ for (const k of [...zone.transformers, ...zone.edges]) {
202
+ const l = snapshot.branches[k]?.loading;
203
+ if (l !== null && l !== undefined && Number.isFinite(l))
204
+ loading = Math.max(loading, l);
205
+ }
206
+ return {
207
+ minVolts: [pn, pg, pp].find(Number.isFinite) ?? null,
208
+ maxLoading: Number.isFinite(loading) ? loading : null
209
+ };
210
+ }
@@ -0,0 +1,18 @@
1
+ import type { DistGraph, DistGraphBus, DistGraphEdge } from '@tellegen/engine';
2
+ import type { SearchEntity } from './search.js';
3
+ import type { BusLimits, BranchRating, SnapshotBranch, SnapshotBus } from './snapshot.js';
4
+ import type { TableNames } from './results-table.js';
5
+ /** Everything the workbench derives from one multiconductor graph, in graph order. */
6
+ export interface GraphInputs {
7
+ buses: SnapshotBus[];
8
+ branches: SnapshotBranch[];
9
+ names: TableNames;
10
+ entities: SearchEntity[];
11
+ }
12
+ /** Declared bands only, each with both a minimum and a maximum. Phase–phase bounds are ordered by
13
+ * the pairs (1,2), (2,3), (3,1) of the phase terminals (BMOPF 0.2.0), which the engine names in
14
+ * `vpp_pairs`; without those names only a uniform band is taken, as there is nothing to place the
15
+ * others by. Sequence and neutral-to-ground bounds are not ranked. */
16
+ export declare function busLimits(bus: Pick<DistGraphBus, 'voltage_limits' | 'terminals'>): BusLimits | null;
17
+ export declare function edgeRating(edge: Pick<DistGraphEdge, 'rating'>): BranchRating | null;
18
+ export declare function graphInputs(graph: DistGraph): GraphInputs;
@@ -0,0 +1,103 @@
1
+ /** Declared bands only, each with both a minimum and a maximum. Phase–phase bounds are ordered by
2
+ * the pairs (1,2), (2,3), (3,1) of the phase terminals (BMOPF 0.2.0), which the engine names in
3
+ * `vpp_pairs`; without those names only a uniform band is taken, as there is nothing to place the
4
+ * others by. Sequence and neutral-to-ground bounds are not ranked. */
5
+ export function busLimits(bus) {
6
+ const l = bus.voltage_limits;
7
+ if (!l)
8
+ return null;
9
+ const limits = {};
10
+ if (l.vpn_min?.length && l.vpn_max?.length === l.vpn_min.length)
11
+ limits.phaseNeutral = { min: l.vpn_min, max: l.vpn_max };
12
+ const [lo, hi, pairs] = [l.vpp_min, l.vpp_max, l.vpp_pairs];
13
+ if (pairs?.length && lo?.length === pairs.length && hi?.length === pairs.length) {
14
+ limits.phasePhase = pairs.map((pair, k) => ({ pair, min: lo[k], max: hi[k] }));
15
+ }
16
+ else {
17
+ const uniform = (v) => (v?.length && v.every((x) => x === v[0]) ? v[0] : null);
18
+ const min = uniform(lo);
19
+ const max = uniform(hi);
20
+ if (min !== null && max !== null)
21
+ limits.phasePhase = [{ pair: null, min, max }];
22
+ }
23
+ const below = l.v_min_by_terminal ?? {};
24
+ const above = l.v_max_by_terminal ?? {};
25
+ const ground = bus.terminals.flatMap((terminal) => Object.hasOwn(below, terminal) && Object.hasOwn(above, terminal)
26
+ ? [{ terminal, min: below[terminal], max: above[terminal] }]
27
+ : []);
28
+ if (ground.length)
29
+ limits.phaseGround = ground;
30
+ return limits.phaseNeutral || limits.phasePhase || limits.phaseGround ? limits : null;
31
+ }
32
+ export function edgeRating(edge) {
33
+ const r = edge.rating;
34
+ if (r?.i_max?.length)
35
+ return { kind: 'current', amps: r.i_max };
36
+ const byBus = r?.s_rating_by_bus;
37
+ if (byBus && Object.values(byBus).some((va) => va > 0))
38
+ return { kind: 'apparent', byBus };
39
+ return null;
40
+ }
41
+ export function graphInputs(graph) {
42
+ const index = new Map(graph.buses.map((b, i) => [b.id.toLowerCase(), i]));
43
+ const busIndex = (id) => index.get(id.toLowerCase()) ?? -1;
44
+ const buses = graph.buses.map((b) => ({
45
+ id: b.id,
46
+ terminals: b.terminals,
47
+ // Kept as given: `null` declares no neutral, absent leaves the aliases to decide.
48
+ neutral: b.neutral_terminal,
49
+ limits: busLimits(b)
50
+ }));
51
+ const branches = graph.edges.map((e) => ({
52
+ id: e.id,
53
+ kind: e.kind,
54
+ from: e.from,
55
+ to: e.to,
56
+ fromTerminals: e.conductors.map((c) => c[0]),
57
+ toTerminals: e.conductors.map((c) => c[1]),
58
+ inService: e.closed,
59
+ rating: edgeRating(e)
60
+ }));
61
+ return {
62
+ buses,
63
+ branches,
64
+ names: {
65
+ buses: graph.buses.map((b) => ({ id: b.id })),
66
+ branches: graph.edges.map((e) => ({
67
+ id: e.id,
68
+ family: e.kind,
69
+ from: busIndex(e.from),
70
+ to: busIndex(e.to),
71
+ inService: e.closed
72
+ }))
73
+ },
74
+ entities: [
75
+ ...graph.buses.map((b, i) => ({
76
+ kind: 'bus',
77
+ index: i,
78
+ id: b.id,
79
+ family: 'bus',
80
+ qualified: b.id
81
+ })),
82
+ // One entry per piece of equipment: a multi-winding transformer projects to one edge per
83
+ // winding pair, all sharing its id, and is found once, at its first edge.
84
+ ...firstEdges(graph).map((i) => {
85
+ const e = graph.edges[i];
86
+ return { kind: 'branch', index: i, id: e.id, family: e.kind, qualified: e.id };
87
+ })
88
+ ]
89
+ };
90
+ }
91
+ /** Index of the first edge of each kind and id, in graph order. */
92
+ function firstEdges(graph) {
93
+ const seen = new Set();
94
+ const out = [];
95
+ graph.edges.forEach((e, i) => {
96
+ const key = `${e.kind}\u0000${e.id}`;
97
+ if (seen.has(key))
98
+ return;
99
+ seen.add(key);
100
+ out.push(i);
101
+ });
102
+ return out;
103
+ }
@@ -0,0 +1,28 @@
1
+ /** Engineering diagram workbench logic, ported from the diagram-projection prototype in PR #177.
2
+ * Pure and renderer-independent: components and the deck.gl canvas build on these. */
3
+ export * from './snapshot.js';
4
+ export * from './summary.js';
5
+ export * from './colour-modes.js';
6
+ export * from './search.js';
7
+ export * from './results-table.js';
8
+ export * from './value-labels.js';
9
+ export * from './svg-export.js';
10
+ export * from './layout-morph.js';
11
+ export * from './phase-bars.js';
12
+ export * from './view-state.js';
13
+ export * from './graph-inputs.js';
14
+ export * from './display-tree.js';
15
+ export * from './schematic-layout.js';
16
+ export * from './multi-wire.js';
17
+ export * from './schematic-equipment.js';
18
+ export * from './energisation.js';
19
+ export * from './short-names.js';
20
+ export * from './tree-walk.js';
21
+ export * from './galvanic-zones.js';
22
+ export * from './voltage-levels.js';
23
+ export * from './feeders.js';
24
+ export * from './connections.js';
25
+ export * from './landmarks.js';
26
+ export * from './basemap.js';
27
+ export * from './shared-routes.js';
28
+ export * from './bus-stacks.js';
@@ -0,0 +1,28 @@
1
+ /** Engineering diagram workbench logic, ported from the diagram-projection prototype in PR #177.
2
+ * Pure and renderer-independent: components and the deck.gl canvas build on these. */
3
+ export * from './snapshot.js';
4
+ export * from './summary.js';
5
+ export * from './colour-modes.js';
6
+ export * from './search.js';
7
+ export * from './results-table.js';
8
+ export * from './value-labels.js';
9
+ export * from './svg-export.js';
10
+ export * from './layout-morph.js';
11
+ export * from './phase-bars.js';
12
+ export * from './view-state.js';
13
+ export * from './graph-inputs.js';
14
+ export * from './display-tree.js';
15
+ export * from './schematic-layout.js';
16
+ export * from './multi-wire.js';
17
+ export * from './schematic-equipment.js';
18
+ export * from './energisation.js';
19
+ export * from './short-names.js';
20
+ export * from './tree-walk.js';
21
+ export * from './galvanic-zones.js';
22
+ export * from './voltage-levels.js';
23
+ export * from './feeders.js';
24
+ export * from './connections.js';
25
+ export * from './landmarks.js';
26
+ export * from './basemap.js';
27
+ export * from './shared-routes.js';
28
+ export * from './bus-stacks.js';
@@ -0,0 +1,50 @@
1
+ import type { DistGraph } from '@tellegen/engine';
2
+ import type { GalvanicZones, ZoneState } from './galvanic-zones.js';
3
+ import { type EquipmentSymbols, type Glyph } from './schematic-equipment.js';
4
+ import type { SchematicGeometry } from './schematic-layout.js';
5
+ /** Equipment that should stay findable when the drawing is zoomed out: drawn at a fixed screen
6
+ * size, placed by priority so the overview never fills with markers. */
7
+ export interface Landmark {
8
+ kind: 'source' | 'generator' | 'ibr' | 'transformer';
9
+ x: number;
10
+ y: number;
11
+ /** Direction along the branch, for transformers. */
12
+ cos: number;
13
+ sin: number;
14
+ /** Lower is placed first: 0 sources (always), 1 transformers whose network has a problem,
15
+ * 2 machines and large IBRs, 3 other transformers, 4 small IBRs. */
16
+ tier: number;
17
+ /** Within a tier, larger first: generation in W, or the fed network's bus count. */
18
+ weight: number;
19
+ state: ZoneState | null;
20
+ ids: string[];
21
+ bus: number;
22
+ edge: number;
23
+ /** The marker, when it differs by unit: a transformer's winding marks or centre tap. */
24
+ glyph?: Glyph;
25
+ }
26
+ /** An IBR counts as large from this share of all generation and IBR output upwards. */
27
+ export declare const LARGE_IBR_SHARE = 0.01;
28
+ /** Landmark candidates in priority order. Generators and IBRs are grouped per bus and kind, sized by
29
+ * solved output when `powers` is given, else by the bus's declared generation. */
30
+ export declare function landmarkCandidates(graph: DistGraph, geometry: SchematicGeometry, symbols: EquipmentSymbols | null, zones: GalvanicZones, zoneStates: ZoneState[], powers: Map<string, {
31
+ p: number;
32
+ q: number;
33
+ }> | null): Landmark[];
34
+ /** Greedy placement on a coarse screen grid: sources always; others only inside the view, on a
35
+ * free cell (and its neighbours), up to `budget` markers. */
36
+ export declare function placeLandmarks(candidates: Landmark[], toScreen: (x: number, y: number) => [number, number], view: {
37
+ width: number;
38
+ height: number;
39
+ cell?: number;
40
+ budget?: number;
41
+ }): Landmark[];
42
+ /** The marker glyph for a landmark, centred on the origin: the symbol itself without its lead. */
43
+ export declare function landmarkGlyph(kind: Landmark['kind']): Glyph;
44
+ /** Stroke segments for placed landmarks, `unit` drawing units per glyph unit (a fixed number of
45
+ * pixels at the current zoom), with the landmark index of each segment. */
46
+ export declare function landmarkStrokes(placed: Landmark[], unit: number): {
47
+ sources: Float32Array;
48
+ targets: Float32Array;
49
+ owner: Int32Array;
50
+ };
@@ -0,0 +1,153 @@
1
+ import { attachmentGlyph, branchGlyph } from './schematic-equipment.js';
2
+ import { transformerGlyph, transformerInfo } from './connections.js';
3
+ /** An IBR counts as large from this share of all generation and IBR output upwards. */
4
+ export const LARGE_IBR_SHARE = 0.01;
5
+ /** Landmark candidates in priority order. Generators and IBRs are grouped per bus and kind, sized by
6
+ * solved output when `powers` is given, else by the bus's declared generation. */
7
+ export function landmarkCandidates(graph, geometry, symbols, zones, zoneStates, powers) {
8
+ const out = [];
9
+ const at = (i) => [geometry.positions[i * 3], geometry.positions[i * 3 + 1]];
10
+ const gens = [];
11
+ graph.buses.forEach((bus, i) => {
12
+ const byKind = new Map();
13
+ for (const list of Object.values(bus.terminal_attachments ?? {}))
14
+ for (const a of list)
15
+ byKind.set(a.kind, (byKind.get(a.kind) ?? new Set()).add(a.id));
16
+ const [x, y] = at(i);
17
+ const base = { x, y, cos: 1, sin: 0, state: null, bus: i, edge: -1 };
18
+ if (bus.has_source || byKind.has('source'))
19
+ out.push({
20
+ ...base,
21
+ kind: 'source',
22
+ tier: 0,
23
+ weight: 0,
24
+ ids: [...(byKind.get('source') ?? [bus.id])]
25
+ });
26
+ for (const kind of ['generator', 'ibr']) {
27
+ const ids = [...(byKind.get(kind) ?? [])];
28
+ if (!ids.length)
29
+ continue;
30
+ const solved = powers
31
+ ? ids.reduce((sum, id) => sum + Math.max(0, -(powers.get(id)?.p ?? 0)), 0)
32
+ : 0;
33
+ const weight = solved || Math.max(0, bus.gen_kw * 1000);
34
+ gens.push({ ...base, kind, tier: 2, weight, ids });
35
+ }
36
+ });
37
+ const total = gens.reduce((sum, g) => sum + g.weight, 0);
38
+ for (const g of gens)
39
+ if (g.kind === 'ibr' && !(total > 0 && g.weight >= LARGE_IBR_SHARE * total))
40
+ g.tier = 4;
41
+ out.push(...gens);
42
+ // The network each transformer feeds, for its state and size.
43
+ const fed = new Map();
44
+ zones.zones.forEach((z, zi) => {
45
+ if (z.transformers.length && !z.hasSource)
46
+ fed.set(z.transformers[0], zi);
47
+ });
48
+ const index = new Map(graph.buses.map((b, i) => [b.id.toLowerCase(), i]));
49
+ // One marker per transformer, on its first winding-pair edge.
50
+ const seen = new Set();
51
+ graph.edges.forEach((edge, k) => {
52
+ if (edge.kind !== 'transformer' || seen.has(edge.id))
53
+ return;
54
+ seen.add(edge.id);
55
+ let x = symbols?.anchors[k * 2] ?? NaN;
56
+ let y = symbols?.anchors[k * 2 + 1] ?? NaN;
57
+ // A vertical branch has cos exactly 0, so test for a missing direction, not a falsy one.
58
+ const fx = symbols?.flowDir[k * 2] ?? 0;
59
+ const fy = symbols?.flowDir[k * 2 + 1] ?? 0;
60
+ const known = fx !== 0 || fy !== 0;
61
+ let cos = known ? fx : 1;
62
+ let sin = known ? fy : 0;
63
+ if (!Number.isFinite(x)) {
64
+ const a = index.get(edge.from.toLowerCase());
65
+ const b = index.get(edge.to.toLowerCase());
66
+ if (a === undefined || b === undefined)
67
+ return;
68
+ const [ax, ay] = at(a);
69
+ const [bx, by] = at(b);
70
+ [x, y] = [(ax + bx) / 2, (ay + by) / 2];
71
+ const len = Math.hypot(bx - ax, by - ay) || 1;
72
+ [cos, sin] = [(bx - ax) / len, (by - ay) / len];
73
+ }
74
+ const zi = fed.get(k);
75
+ const state = zi === undefined ? null : zoneStates[zi];
76
+ const problem = state === 'violation' || state === 'warning';
77
+ out.push({
78
+ kind: 'transformer',
79
+ x,
80
+ y,
81
+ cos,
82
+ sin,
83
+ tier: problem ? 1 : 3,
84
+ weight: zi === undefined ? 0 : zones.zones[zi].buses.length,
85
+ state,
86
+ ids: [edge.id],
87
+ bus: -1,
88
+ edge: k,
89
+ glyph: transformerGlyph(transformerInfo(graph, k), edge.n_phases, false)
90
+ });
91
+ });
92
+ return out.sort((a, b) => a.tier - b.tier || b.weight - a.weight);
93
+ }
94
+ /** Greedy placement on a coarse screen grid: sources always; others only inside the view, on a
95
+ * free cell (and its neighbours), up to `budget` markers. */
96
+ export function placeLandmarks(candidates, toScreen, view) {
97
+ const cell = view.cell ?? 28;
98
+ const budget = view.budget ?? 60;
99
+ const taken = new Set();
100
+ const placed = [];
101
+ for (const c of candidates) {
102
+ const [sx, sy] = toScreen(c.x, c.y);
103
+ const inside = sx >= 0 && sx <= view.width && sy >= 0 && sy <= view.height;
104
+ if (!inside)
105
+ continue;
106
+ const gx = Math.floor(sx / cell);
107
+ const gy = Math.floor(sy / cell);
108
+ if (c.tier > 0) {
109
+ if (placed.length >= budget)
110
+ break;
111
+ let free = true;
112
+ for (let dx = -1; dx <= 1 && free; dx++)
113
+ for (let dy = -1; dy <= 1 && free; dy++)
114
+ if (taken.has(`${gx + dx},${gy + dy}`))
115
+ free = false;
116
+ if (!free)
117
+ continue;
118
+ }
119
+ taken.add(`${gx},${gy}`);
120
+ placed.push(c);
121
+ }
122
+ return placed;
123
+ }
124
+ /** The marker glyph for a landmark, centred on the origin: the symbol itself without its lead. */
125
+ export function landmarkGlyph(kind) {
126
+ if (kind === 'transformer')
127
+ return branchGlyph('transformer', true).slice(1, 3);
128
+ return attachmentGlyph(kind)
129
+ .slice(1)
130
+ .map((line) => line.map(([x, y]) => [x - 1.3, y]));
131
+ }
132
+ /** Stroke segments for placed landmarks, `unit` drawing units per glyph unit (a fixed number of
133
+ * pixels at the current zoom), with the landmark index of each segment. */
134
+ export function landmarkStrokes(placed, unit) {
135
+ const src = [];
136
+ const dst = [];
137
+ const owner = [];
138
+ placed.forEach((l, j) => {
139
+ for (const line of l.glyph ?? landmarkGlyph(l.kind))
140
+ for (let i = 1; i < line.length; i++) {
141
+ const [ax, ay] = line[i - 1];
142
+ const [bx, by] = line[i];
143
+ src.push(l.x + unit * (ax * l.cos - ay * l.sin), l.y + unit * (ax * l.sin + ay * l.cos), 0);
144
+ dst.push(l.x + unit * (bx * l.cos - by * l.sin), l.y + unit * (bx * l.sin + by * l.cos), 0);
145
+ owner.push(j);
146
+ }
147
+ });
148
+ return {
149
+ sources: Float32Array.from(src),
150
+ targets: Float32Array.from(dst),
151
+ owner: Int32Array.from(owner)
152
+ };
153
+ }
@@ -0,0 +1,15 @@
1
+ /** Animated transition between two layouts of the same buses, so the eye can follow a feeder. */
2
+ export declare const MORPH_MS = 900;
3
+ /** Measured smooth up to 98,633 buses on the reference machine (M4 Max, #177 M0 evidence, with a
4
+ * warm-up frame); larger drawings switch instantly until measured. */
5
+ export declare const MORPH_MAX_BUSES = 100000;
6
+ export interface Camera {
7
+ target: ArrayLike<number>;
8
+ zoom: number;
9
+ }
10
+ export declare const ease: (t: number) => number;
11
+ /** Re-express layout A positions in layout B's world, so each bus starts where it is on screen. */
12
+ export declare function screenAnchored(positions: ArrayLike<number>, from: Camera, to: Camera): Float32Array;
13
+ export declare function lerpInto(out: Float32Array, a: ArrayLike<number>, b: ArrayLike<number>, t: number): Float32Array<ArrayBufferLike>;
14
+ /** Straight bus-to-bus chords while moving; routed geometry returns when the transition ends. */
15
+ export declare function chordsInto(sources: Float32Array, targets: Float32Array, endpoints: ArrayLike<number>, positions: ArrayLike<number>): void;
@@ -0,0 +1,32 @@
1
+ /** Animated transition between two layouts of the same buses, so the eye can follow a feeder. */
2
+ export const MORPH_MS = 900;
3
+ /** Measured smooth up to 98,633 buses on the reference machine (M4 Max, #177 M0 evidence, with a
4
+ * warm-up frame); larger drawings switch instantly until measured. */
5
+ export const MORPH_MAX_BUSES = 100_000;
6
+ export const ease = (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
7
+ /** Re-express layout A positions in layout B's world, so each bus starts where it is on screen. */
8
+ export function screenAnchored(positions, from, to) {
9
+ const k = 2 ** from.zoom / 2 ** to.zoom;
10
+ const out = new Float32Array(positions.length);
11
+ for (let i = 0; i < positions.length; i += 3) {
12
+ out[i] = to.target[0] + (positions[i] - from.target[0]) * k;
13
+ out[i + 1] = to.target[1] + (positions[i + 1] - from.target[1]) * k;
14
+ }
15
+ return out;
16
+ }
17
+ export function lerpInto(out, a, b, t) {
18
+ for (let i = 0; i < out.length; i++)
19
+ out[i] = a[i] + (b[i] - a[i]) * t;
20
+ return out;
21
+ }
22
+ /** Straight bus-to-bus chords while moving; routed geometry returns when the transition ends. */
23
+ export function chordsInto(sources, targets, endpoints, positions) {
24
+ for (let b = 0; b < endpoints.length / 2; b++) {
25
+ const u = endpoints[b * 2] * 3;
26
+ const v = endpoints[b * 2 + 1] * 3;
27
+ for (let k = 0; k < 3; k++) {
28
+ sources[b * 3 + k] = positions[u + k];
29
+ targets[b * 3 + k] = positions[v + k];
30
+ }
31
+ }
32
+ }