@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,83 @@
1
+ import { ATTACHMENT_NAMES, ATTACHMENT_ORDER, neutralEarths } from './schematic-equipment.js';
2
+ export function treeWalk(tree, positions, stride = 3) {
3
+ const n = tree.parent.length;
4
+ const y = (i) => positions[i * stride + 1];
5
+ const x = (i) => positions[i * stride];
6
+ const order = (a, b) => y(a) - y(b) || x(a) - x(b) || a - b;
7
+ const children = Array.from({ length: n }, () => []);
8
+ for (let i = 0; i < n; i++)
9
+ if (tree.parent[i] !== i)
10
+ children[tree.parent[i]].push(i);
11
+ for (const list of children)
12
+ list.sort(order);
13
+ const roots = [...tree.roots].sort(order);
14
+ const siblings = (i) => (tree.parent[i] === i ? roots : children[tree.parent[i]]);
15
+ return {
16
+ children,
17
+ step(from, key) {
18
+ if (from < 0 || from >= n)
19
+ return roots[0] ?? -1;
20
+ switch (key) {
21
+ case 'parent':
22
+ return tree.parent[from];
23
+ case 'child': {
24
+ // Straight ahead: the child drawn nearest this bus's row.
25
+ const kids = children[from];
26
+ if (!kids.length)
27
+ return from;
28
+ return kids.reduce((best, k) => Math.abs(y(k) - y(from)) < Math.abs(y(best) - y(from)) ? k : best);
29
+ }
30
+ case 'previous':
31
+ case 'next': {
32
+ const list = siblings(from);
33
+ const at = list.indexOf(from);
34
+ const to = at + (key === 'next' ? 1 : -1);
35
+ return to >= 0 && to < list.length ? list[to] : from;
36
+ }
37
+ case 'root': {
38
+ let i = from;
39
+ while (tree.parent[i] !== i)
40
+ i = tree.parent[i];
41
+ return i;
42
+ }
43
+ }
44
+ }
45
+ };
46
+ }
47
+ /** A spoken description of bus `i` for the keyboard cursor: where it sits in the tree, what hangs
48
+ * off it, and an optional result note (for example the worst voltage against its band). */
49
+ export function describeBus(graph, tree, walk, i, note) {
50
+ const bus = graph.buses[i];
51
+ let root = i;
52
+ while (tree.parent[root] !== root)
53
+ root = tree.parent[root];
54
+ const depth = tree.depth[i];
55
+ const where = depth === 0
56
+ ? bus.has_source
57
+ ? 'source bus'
58
+ : 'not reached from a source'
59
+ : `${depth} step${depth === 1 ? '' : 's'} from ${graph.buses[root].id}`;
60
+ const via = tree.parentEdge[i] >= 0
61
+ ? `, fed by ${graph.edges[tree.parentEdge[i]].kind} ${graph.edges[tree.parentEdge[i]].id}`
62
+ : '';
63
+ const onward = walk.children[i].length;
64
+ const counts = new Map();
65
+ const earths = neutralEarths(bus);
66
+ for (const list of Object.values(bus.terminal_attachments ?? {}))
67
+ for (const a of list) {
68
+ const kind = a.kind === 'shunt' && earths.has(a.id) ? 'earth' : a.kind;
69
+ counts.set(kind, (counts.get(kind) ?? new Set()).add(a.id));
70
+ }
71
+ if (bus.grounded?.length)
72
+ counts.set('ground', new Set(bus.grounded));
73
+ const attached = ATTACHMENT_ORDER.filter((k) => counts.has(k)).map((k) => {
74
+ const c = counts.get(k).size;
75
+ return `${c} ${ATTACHMENT_NAMES[k]}${c === 1 ? '' : 's'}`;
76
+ });
77
+ return [
78
+ `${bus.id}: ${where}${via}`,
79
+ `${onward} branch${onward === 1 ? '' : 'es'} onward`,
80
+ ...(attached.length ? [attached.join(', ')] : []),
81
+ ...(note ? [note] : [])
82
+ ].join('. ');
83
+ }
@@ -0,0 +1,24 @@
1
+ import type { ResultSnapshot } from './snapshot.js';
2
+ export declare const VALUE_LABEL_BUDGET = 60;
3
+ export declare const MIN_LABEL_ZOOM = -0.2;
4
+ export type LabelState = 'violation' | 'warning' | 'normal';
5
+ export interface LabelCandidate {
6
+ text: string;
7
+ state: LabelState;
8
+ priority: number;
9
+ /** Screen position in CSS pixels. */
10
+ screen: [number, number];
11
+ selected?: boolean;
12
+ }
13
+ /** Greedy placement by (selected, severity, priority) on a coarse collision grid. The selection is
14
+ * always placed; others must fit inside the canvas margins and the budget. */
15
+ export declare function placeLabels<T extends LabelCandidate>(candidates: T[], { width, height, budget, box, margin }: {
16
+ width: number;
17
+ height: number;
18
+ budget?: number;
19
+ box?: [number, number];
20
+ margin?: [number, number, number, number];
21
+ }): T[];
22
+ /** The pair nearest a limit, shown in volts. Buses without declared limits rank last. */
23
+ export declare function busLabel(result: ResultSnapshot, bus: number): Omit<LabelCandidate, 'screen'> | null;
24
+ export declare function branchLabel(result: ResultSnapshot, branch: number, family: 'loading' | 'power'): Omit<LabelCandidate, 'screen'> | null;
@@ -0,0 +1,69 @@
1
+ import { bandPosition } from './colour-modes.js';
2
+ import { loadingState } from './summary.js';
3
+ export const VALUE_LABEL_BUDGET = 60;
4
+ export const MIN_LABEL_ZOOM = -0.2;
5
+ const SEVERITY = { violation: 2, warning: 1, normal: 0 };
6
+ /** Greedy placement by (selected, severity, priority) on a coarse collision grid. The selection is
7
+ * always placed; others must fit inside the canvas margins and the budget. */
8
+ export function placeLabels(candidates, { width, height, budget = VALUE_LABEL_BUDGET, box = [64, 16], margin = [0, 40, 0, 24] }) {
9
+ const sorted = [...candidates].sort((a, b) => Number(!!b.selected) - Number(!!a.selected) ||
10
+ SEVERITY[b.state] - SEVERITY[a.state] ||
11
+ b.priority - a.priority);
12
+ const [cw, ch] = box;
13
+ const occupied = new Set();
14
+ const placed = [];
15
+ for (const c of sorted) {
16
+ if (placed.length >= budget)
17
+ break;
18
+ const [sx, sy] = c.screen;
19
+ const outside = sx < margin[3] ||
20
+ sx + cw > width - margin[1] ||
21
+ sy - ch < margin[0] ||
22
+ sy > height - margin[2];
23
+ if (!c.selected && outside)
24
+ continue;
25
+ const keys = [];
26
+ for (let gy = Math.floor((sy - ch) / ch); gy <= Math.floor(sy / ch); gy++)
27
+ for (let gx = Math.floor(sx / cw); gx <= Math.floor((sx + cw) / cw); gx++)
28
+ keys.push(`${gx},${gy}`);
29
+ if (!c.selected && keys.some((k) => occupied.has(k)))
30
+ continue;
31
+ for (const k of keys)
32
+ occupied.add(k);
33
+ placed.push(c);
34
+ }
35
+ return placed;
36
+ }
37
+ const short = (x) => Number(x.toPrecision(Math.abs(x) >= 1000 ? 5 : 4)).toLocaleString();
38
+ /** The pair nearest a limit, shown in volts. Buses without declared limits rank last. */
39
+ export function busLabel(result, bus) {
40
+ const pairs = result.buses[bus].pairs;
41
+ if (!pairs.length)
42
+ return null;
43
+ // Distance from mid-band; −1 without declared limits, so those pairs and buses rank last.
44
+ const rank = (p) => {
45
+ const band = bandPosition(p);
46
+ return band === null ? -1 : Math.abs(band);
47
+ };
48
+ let pick = pairs[0];
49
+ let t = rank(pick);
50
+ for (const p of pairs) {
51
+ const pt = rank(p);
52
+ if (pt > t)
53
+ [pick, t] = [p, pt];
54
+ }
55
+ return { text: `${short(pick.volts)} V`, state: t > 1 ? 'violation' : 'normal', priority: t };
56
+ }
57
+ export function branchLabel(result, branch, family) {
58
+ const row = result.branches[branch];
59
+ if (family === 'loading') {
60
+ const state = loadingState(row.loading);
61
+ return state === 'missing'
62
+ ? null
63
+ : { text: `${row.loading.toFixed(1)} %`, state, priority: row.loading };
64
+ }
65
+ const end = row.ends[0] ?? row.ends[1];
66
+ return end
67
+ ? { text: `${short(Math.abs(end.p))} kW`, state: 'normal', priority: Math.abs(end.p) }
68
+ : null;
69
+ }
@@ -0,0 +1,12 @@
1
+ /** Shareable view state. Selections travel as entity keys (source identities), never array offsets. */
2
+ export interface CameraState {
3
+ target: [number, number, number];
4
+ zoom: number;
5
+ }
6
+ export declare function encodeCamera(c: {
7
+ target: ArrayLike<number>;
8
+ zoom: number;
9
+ }): string;
10
+ export declare function decodeCamera(text: string | null | undefined): CameraState | null;
11
+ /** Merge state into a URL without dropping unrelated parameters; `null` or `''` removes a key. */
12
+ export declare function withView(href: string | URL, state: Record<string, string | number | null | undefined>): URL;
@@ -0,0 +1,22 @@
1
+ export function encodeCamera(c) {
2
+ return [c.target[0].toFixed(1), c.target[1].toFixed(1), c.zoom.toFixed(2)].join(',');
3
+ }
4
+ export function decodeCamera(text) {
5
+ const parts = String(text ?? '')
6
+ .split(',')
7
+ .map(Number);
8
+ return parts.length === 3 && parts.every(Number.isFinite) && parts[2] >= -24 && parts[2] <= 5
9
+ ? { target: [parts[0], parts[1], 0], zoom: parts[2] }
10
+ : null;
11
+ }
12
+ /** Merge state into a URL without dropping unrelated parameters; `null` or `''` removes a key. */
13
+ export function withView(href, state) {
14
+ const url = new URL(href);
15
+ for (const [k, v] of Object.entries(state)) {
16
+ if (v === null || v === undefined || v === '')
17
+ url.searchParams.delete(k);
18
+ else
19
+ url.searchParams.set(k, String(v));
20
+ }
21
+ return url;
22
+ }
@@ -0,0 +1,14 @@
1
+ import type { GalvanicZone } from './galvanic-zones.js';
2
+ /** Voltage classes for drawing, by a zone's level in volts. Wide bands, so a single-phase winding's
3
+ * own rating (7.2 kV of a 12.47 kV system, 19.1 kV SWER) lands where its system does. */
4
+ export type VoltageClass = 'hv' | 'sub' | 'mv' | 'lv';
5
+ export declare const VOLTAGE_CLASSES: {
6
+ cls: VoltageClass;
7
+ name: string;
8
+ from: number;
9
+ }[];
10
+ /** The class of a zone: by its level when known; otherwise the zone holding a source counts as MV
11
+ * and the others as LV, as for line weights. */
12
+ export declare function voltageClass(zone: GalvanicZone, volts: number | null): VoltageClass;
13
+ /** A level as engineers name it: "11 kV", "400 V", "19.1 kV". */
14
+ export declare function levelLabel(volts: number): string;
@@ -0,0 +1,20 @@
1
+ export const VOLTAGE_CLASSES = [
2
+ { cls: 'hv', name: 'transmission', from: 100_000 },
3
+ { cls: 'sub', name: 'subtransmission', from: 25_000 },
4
+ { cls: 'mv', name: 'MV', from: 1_000 },
5
+ { cls: 'lv', name: 'LV', from: 0 }
6
+ ];
7
+ /** The class of a zone: by its level when known; otherwise the zone holding a source counts as MV
8
+ * and the others as LV, as for line weights. */
9
+ export function voltageClass(zone, volts) {
10
+ if (volts === null)
11
+ return zone.hasSource ? 'mv' : 'lv';
12
+ return VOLTAGE_CLASSES.find((c) => volts >= c.from).cls;
13
+ }
14
+ /** A level as engineers name it: "11 kV", "400 V", "19.1 kV". */
15
+ export function levelLabel(volts) {
16
+ const kv = volts / 1000;
17
+ if (kv < 1)
18
+ return `${Math.round(volts)} V`;
19
+ return `${Number(kv.toPrecision(4))} kV`;
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { getNoticeCenter, errorNoticeTitle } from './notices.svelte.js';
2
+ export { DiagnosticLog, LOG_SEVERITIES, errorEntry, errorStage, logSeverity, powerIoEntry, solveEntry } from './log.svelte.js';
3
+ export type { LogDocument, LogEntry, LogEntryInput, LogSeverity } from './log.svelte.js';
2
4
  export { default as ModelDetails } from './components/ModelDetails.svelte';
3
5
  export { default as PanelFrame } from './components/PanelFrame.svelte';
4
6
  export { getPanelLayout } from './panels.svelte.js';
@@ -27,3 +29,4 @@ export type { BusSizing, CameraSnapshot, DiagramCameraSnapshot, DemandRangeMode,
27
29
  export type { MultiView, PlacedMultiBus, PlacedMultiEdge } from './multiconductor.js';
28
30
  export type { CaseFileSummary, DistAttachmentKind, DistEdgeKind, DistGraph, DistGraphAttachment, DistGraphBus, DistGraphEdge, DisplayPreview, Formulation, IngestedCase, IngestedDistCase, IngestedJsonDrop, JsonDropClassification, JsonDropKind, CapacityPlanBusWeightJson, CapacityPlanIterationJson, CapacityPlanOutcomeJson, CapacityPlanRatingChangeJson, CapacityPlanResultSummaryJson, CapacityPlanSpecJson, ImplicitObjectiveJson, SensTarget, Topology, TopologyBranch, TopologyBus } from '@tellegen/engine';
29
31
  export { default as McResults } from './components/McResults.svelte';
32
+ export { default as McWorkbench } from './components/McWorkbench.svelte';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { getNoticeCenter, errorNoticeTitle } from './notices.svelte.js';
2
+ export { DiagnosticLog, LOG_SEVERITIES, errorEntry, errorStage, logSeverity, powerIoEntry, solveEntry } from './log.svelte.js';
2
3
  export { default as ModelDetails } from './components/ModelDetails.svelte';
3
4
  export { default as PanelFrame } from './components/PanelFrame.svelte';
4
5
  export { getPanelLayout } from './panels.svelte.js';
@@ -17,3 +18,4 @@ export { distExtensionFormat } from './drop-classify.js';
17
18
  export { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './display.js';
18
19
  export { DEFAULT_FORMULATION, FORMULATIONS, BrowserStudy, Study, capabilities, createStudy, errorText, formatOf, classifyJson, ingestJsonDrop, ingestCase, isDisplayFile, isPermanentEngineFailure, parseDisplay, solveModule } from '@tellegen/engine';
19
20
  export { default as McResults } from './components/McResults.svelte';
21
+ export { default as McWorkbench } from './components/McWorkbench.svelte';
@@ -0,0 +1,77 @@
1
+ import type { PowerIoDiagnostic, SolveDiagnostic } from '@tellegen/engine';
2
+ /** How serious a finding is. PowerIO's `remark` and `note` both read as notes. */
3
+ export type LogSeverity = 'error' | 'warning' | 'note';
4
+ export declare const LOG_SEVERITIES: readonly LogSeverity[];
5
+ /** One finding to record. `stage` names what was happening (`Open`, `Solve`,
6
+ * `Export`, or a host's own step such as `Generate`); `source` names who
7
+ * reported it (`PowerIO`, `tellegen`, or a host's data source). */
8
+ export interface LogEntryInput {
9
+ severity: LogSeverity;
10
+ stage: string;
11
+ source: string;
12
+ message: string;
13
+ /** Stable machine code, such as `EMIT.MATPOWER.FIELD_DROPPED`. */
14
+ code?: string;
15
+ /** The case, file or download the finding belongs to. */
16
+ subject?: string;
17
+ /** The element the finding names, in PowerIO's spelling. */
18
+ target?: string;
19
+ buses?: number[];
20
+ /** What to do about it, when the reporter says. */
21
+ hint?: string;
22
+ details?: Record<string, unknown>;
23
+ }
24
+ export interface LogEntry extends LogEntryInput {
25
+ id: number;
26
+ /** How many times the same finding was reported. */
27
+ count: number;
28
+ /** Epoch milliseconds of the first and the latest report. */
29
+ first: number;
30
+ last: number;
31
+ }
32
+ /** The log as a file: what `DiagnosticLog.toJSON` returns and the log view downloads. */
33
+ export interface LogDocument {
34
+ format: 'tellegen.log';
35
+ version: 1;
36
+ written_at: string;
37
+ counts: Record<LogSeverity, number>;
38
+ entries: (Omit<LogEntry, 'first' | 'last'> & {
39
+ first: string;
40
+ last: string;
41
+ })[];
42
+ }
43
+ /** Every finding the app reported, in the order it first appeared. A finding
44
+ * reported again counts up in place instead of adding a row, so a re-solve or a
45
+ * second export of the same case does not flood the log. */
46
+ export declare class DiagnosticLog {
47
+ #private;
48
+ entries: LogEntry[];
49
+ /** Bumped on every write, including a repeat that only moves `last`. */
50
+ revision: number;
51
+ constructor(now?: () => number);
52
+ /** Record findings. With `repeat: false`, a finding already in the log keeps
53
+ * its count and takes the new details: use it for state that every solve
54
+ * restates, or to add the detail behind an error just recorded. */
55
+ record(input: LogEntryInput | readonly LogEntryInput[], options?: {
56
+ repeat?: boolean;
57
+ }): void;
58
+ clear(): void;
59
+ get counts(): Record<LogSeverity, number>;
60
+ toJSON(): LogDocument;
61
+ }
62
+ export declare function logSeverity(severity: PowerIoDiagnostic['severity']): LogSeverity;
63
+ interface EntryContext {
64
+ stage: string;
65
+ subject?: string;
66
+ }
67
+ /** A PowerIO reader or writer finding as a log entry. */
68
+ export declare function powerIoEntry(d: PowerIoDiagnostic, context: EntryContext): LogEntryInput;
69
+ /** A solver finding (islands, HVDC transfers) as a log entry. */
70
+ export declare function solveEntry(d: SolveDiagnostic, context: {
71
+ subject?: string;
72
+ }): LogEntryInput;
73
+ /** Which step an app error message comes from, by its wording. */
74
+ export declare function errorStage(message: string): string;
75
+ /** An app error as a log entry. */
76
+ export declare function errorEntry(message: string, detail?: string | null): LogEntryInput;
77
+ export {};
@@ -0,0 +1,149 @@
1
+ export const LOG_SEVERITIES = ['error', 'warning', 'note'];
2
+ const MAX_ENTRIES = 500;
3
+ const keyOf = (e) => JSON.stringify([e.severity, e.stage, e.source, e.code, e.subject, e.target, e.message]);
4
+ /** Every finding the app reported, in the order it first appeared. A finding
5
+ * reported again counts up in place instead of adding a row, so a re-solve or a
6
+ * second export of the same case does not flood the log. */
7
+ export class DiagnosticLog {
8
+ entries = $state.raw([]);
9
+ /** Bumped on every write, including a repeat that only moves `last`. */
10
+ revision = $state(0);
11
+ #nextId = 0;
12
+ #now;
13
+ constructor(now = Date.now) {
14
+ this.#now = now;
15
+ }
16
+ /** Record findings. With `repeat: false`, a finding already in the log keeps
17
+ * its count and takes the new details: use it for state that every solve
18
+ * restates, or to add the detail behind an error just recorded. */
19
+ record(input, options = {}) {
20
+ const inputs = Array.isArray(input) ? input : [input];
21
+ if (!inputs.length)
22
+ return;
23
+ const repeat = options.repeat ?? true;
24
+ const now = this.#now();
25
+ const next = [...this.entries];
26
+ const index = new Map(next.map((entry, i) => [keyOf(entry), i]));
27
+ for (const item of inputs) {
28
+ if (!item.message)
29
+ continue;
30
+ const key = keyOf(item);
31
+ const at = index.get(key);
32
+ if (at !== undefined) {
33
+ const previous = next[at];
34
+ next[at] = repeat
35
+ ? { ...previous, count: previous.count + 1, last: now }
36
+ : { ...previous, ...clean(item), last: now };
37
+ continue;
38
+ }
39
+ index.set(key, next.length);
40
+ next.push({
41
+ ...clean(item),
42
+ id: ++this.#nextId,
43
+ count: 1,
44
+ first: now,
45
+ last: now
46
+ });
47
+ }
48
+ this.entries = next.length > MAX_ENTRIES ? next.slice(next.length - MAX_ENTRIES) : next;
49
+ this.revision++;
50
+ }
51
+ clear() {
52
+ this.entries = [];
53
+ this.revision++;
54
+ }
55
+ get counts() {
56
+ const counts = { error: 0, warning: 0, note: 0 };
57
+ for (const entry of this.entries)
58
+ counts[entry.severity]++;
59
+ return counts;
60
+ }
61
+ toJSON() {
62
+ return {
63
+ format: 'tellegen.log',
64
+ version: 1,
65
+ written_at: new Date(this.#now()).toISOString(),
66
+ counts: this.counts,
67
+ entries: this.entries.map((entry) => ({
68
+ ...entry,
69
+ first: new Date(entry.first).toISOString(),
70
+ last: new Date(entry.last).toISOString()
71
+ }))
72
+ };
73
+ }
74
+ }
75
+ function clean(input) {
76
+ const out = {
77
+ severity: input.severity,
78
+ stage: input.stage,
79
+ source: input.source,
80
+ message: input.message
81
+ };
82
+ if (input.code)
83
+ out.code = input.code;
84
+ if (input.subject)
85
+ out.subject = input.subject;
86
+ if (input.target)
87
+ out.target = input.target;
88
+ if (input.buses?.length)
89
+ out.buses = [...input.buses];
90
+ if (input.hint)
91
+ out.hint = input.hint;
92
+ if (input.details && Object.keys(input.details).length)
93
+ out.details = input.details;
94
+ return out;
95
+ }
96
+ export function logSeverity(severity) {
97
+ return severity === 'error' ? 'error' : severity === 'warning' ? 'warning' : 'note';
98
+ }
99
+ /** A PowerIO reader or writer finding as a log entry. */
100
+ export function powerIoEntry(d, context) {
101
+ return {
102
+ severity: logSeverity(d.severity),
103
+ stage: context.stage,
104
+ source: 'PowerIO',
105
+ code: d.code,
106
+ message: d.message,
107
+ subject: context.subject,
108
+ target: d.target,
109
+ hint: d.suggested_action,
110
+ details: d.details
111
+ };
112
+ }
113
+ // Solve findings that change what the numbers mean; the rest say how the
114
+ // solver read the case.
115
+ const SOLVE_WARNINGS = new Set(['island_deenergized', 'BUILD.HVDC.TERMINAL_INACTIVE']);
116
+ /** A solver finding (islands, HVDC transfers) as a log entry. */
117
+ export function solveEntry(d, context) {
118
+ return {
119
+ severity: SOLVE_WARNINGS.has(d.code) ? 'warning' : 'note',
120
+ stage: 'Solve',
121
+ source: 'tellegen',
122
+ code: d.code,
123
+ message: d.message,
124
+ subject: context.subject,
125
+ buses: d.buses
126
+ };
127
+ }
128
+ /** Which step an app error message comes from, by its wording. */
129
+ export function errorStage(message) {
130
+ if (/export|could not save|download/i.test(message))
131
+ return 'Export';
132
+ if (/coordinat|geographic|layout|placement|placed/i.test(message))
133
+ return 'Coordinates';
134
+ if (/infeasib|converg|solv|OPF|power flow|calculation|sensitivity|webassembly|study/i.test(message))
135
+ return 'Solve';
136
+ if (/pars|read|file|format|module/i.test(message))
137
+ return 'Open';
138
+ return 'App';
139
+ }
140
+ /** An app error as a log entry. */
141
+ export function errorEntry(message, detail) {
142
+ return {
143
+ severity: 'error',
144
+ stage: errorStage(message),
145
+ source: 'tellegen',
146
+ message,
147
+ details: detail ? { detail } : undefined
148
+ };
149
+ }
@@ -1,6 +1,9 @@
1
+ import type { LogEntryInput } from './log.svelte.js';
1
2
  export interface NoticeInput {
2
3
  title: string;
3
4
  details: string;
5
+ /** Structured findings behind `details`; the notice lists them when present. */
6
+ entries?: LogEntryInput[];
4
7
  kind?: 'error' | 'warning';
5
8
  retry?: () => void;
6
9
  canRetry?: () => boolean;
@@ -1,7 +1,11 @@
1
1
  import type { CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration } from './api.js';
2
2
  import { type StudyView, type BranchRatingDeltas, type BrowserMcPfSession, type CaseFileSummary, type DistGraph, type Formulation, type IngestedDistCase, type McPfCallTiming, type McPfResult, type McPfSummary, type McLoadBranchState, type McLoadPowerEdit, type McStudySnapshot, type SensTarget, type Topology } from '@tellegen/engine';
3
3
  import type { MultiView } from './multiconductor.js';
4
- export type SolveBackend = 'clarabel-wasm' | 'clarabel-wasm-server-sensitivity' | 'rust-server';
4
+ import { DiagnosticLog } from './log.svelte.js';
5
+ import { type GraphInputs } from './diagram/graph-inputs.js';
6
+ import { type SearchIndex } from './diagram/search.js';
7
+ import { type ResultSnapshot } from './diagram/snapshot.js';
8
+ export type SolveBackend = 'clarabel-wasm' | 'clarabel-wasm-server-sensitivity' | 'pounce-wasi' | 'rust-server';
5
9
  /** A map framing request: a case id, 'all', or one branch to center. */
6
10
  export type FrameTarget = string | 'all' | {
7
11
  caseId: string;
@@ -117,6 +121,8 @@ export declare class LocalCase {
117
121
  solving: boolean;
118
122
  solveMs: number | null;
119
123
  solveBackend: SolveBackend | null;
124
+ /** Short solver-specific diagnostic shown beside the elapsed time. */
125
+ solveDetail: string | null;
120
126
  solveFallbackReason: string | null;
121
127
  /** Monotone token: only the latest solve may write this case. */
122
128
  solveSeq: number;
@@ -155,6 +161,8 @@ export declare class CaseState {
155
161
  solving: boolean;
156
162
  solveMs: number | null;
157
163
  solveBackend: SolveBackend | null;
164
+ /** Short solver-specific diagnostic shown beside the elapsed time. */
165
+ solveDetail: string | null;
158
166
  solveFallbackReason: string | null;
159
167
  /** Monotone token: only the latest solve may write this case. */
160
168
  solveSeq: number;
@@ -231,6 +239,32 @@ export declare class MulticonductorCase {
231
239
  /** The selected edge id, whose conductor pairing expands in the panel;
232
240
  * mutually exclusive with selectedBusId. */
233
241
  selectedEdgeId: string | null;
242
+ /** What the pointer is over in any linked view (schematic, profile, tables); highlighted in
243
+ * the others. Transient: never saved or put in links. */
244
+ hovered: {
245
+ kind: "bus" | "branch";
246
+ id: string;
247
+ } | null;
248
+ /** Bumped to ask the views to bring the current selection into view again (an issue clicked
249
+ * that is already selected). */
250
+ revealSeq: number;
251
+ /** The full operating point read for whole-network views (overview, schematic); null until read,
252
+ * and cleared when a large case's operating point changes. */
253
+ overviewResult: McPfResult | null;
254
+ overviewLoading: boolean;
255
+ /** The graph's buses and branches as the overview and the diagram read them: rebuilt when the
256
+ * graph is replaced, never after a solve. Both views read this one copy. */
257
+ overviewInputs: GraphInputs | null;
258
+ /** Search over the graph's buses and branches, rebuilt with `overviewInputs`. */
259
+ overviewSearch: SearchIndex | null;
260
+ /** `overviewResult` projected onto the graph, built once per operating point and shared by the
261
+ * overview and the diagram; null while there is none. */
262
+ overviewSnapshot: ResultSnapshot | null;
263
+ /** Show this case as a schematic instead of the map. */
264
+ schematic: boolean;
265
+ /** The diagram's last geographic view as a map camera, for the map to continue from when the
266
+ * diagram closes. Plain, not reactive: it is read once, on close. */
267
+ lastGeoView: CameraSnapshot | null;
234
268
  constructor(init: {
235
269
  id: string;
236
270
  label: string;
@@ -264,6 +298,9 @@ export interface StudyDisplaySnapshot {
264
298
  export declare class AppState {
265
299
  #private;
266
300
  camera: CameraSnapshot | null;
301
+ /** Set when a view hands its camera to the map it returns to (the diagram closing from
302
+ * geographic positions): the re-created map starts at `camera` instead of framing the case. */
303
+ cameraHandoff: boolean;
267
304
  cameraRequest: CameraSnapshot | null;
268
305
  cameraSeq: number;
269
306
  diagramCamera: DiagramCameraSnapshot | null;
@@ -303,12 +340,12 @@ export declare class AppState {
303
340
  /** Re-runs the operation behind the current `error`, when one applies. Every
304
341
  * write to `error` clears it, so a retry op can never outlive its message. */
305
342
  errorRetry: (() => void) | null;
306
- /** Technical detail behind the current `error`, when the message itself is
307
- * written for people; notices keep it in their disclosure. Every write to
308
- * `error` clears it, as it clears `errorRetry`. */
309
- errorDetail: string | null;
343
+ /** Every error and engine finding of this session, structured; see `DiagnosticLog`. */
344
+ readonly log: DiagnosticLog;
310
345
  get error(): string | null;
311
346
  set error(message: string | null);
347
+ get errorDetail(): string | null;
348
+ set errorDetail(detail: string | null);
312
349
  /** Case files parsed in the browser via the powerio wasm module. */
313
350
  localCases: LocalCase[];
314
351
  /** Local case the panel shows; clicking a bundled case or a bus clears it. */