@tellegen/svelte 0.1.0
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/LICENSE +19 -0
- package/README.md +70 -0
- package/dist/Sparkline.svelte +100 -0
- package/dist/Sparkline.svelte.d.ts +8 -0
- package/dist/Sparkline.svelte.d.ts.map +1 -0
- package/dist/TellegenMap.svelte +1430 -0
- package/dist/TellegenMap.svelte.d.ts +14 -0
- package/dist/TellegenMap.svelte.d.ts.map +1 -0
- package/dist/TellegenProvider.svelte +38 -0
- package/dist/TellegenProvider.svelte.d.ts +9 -0
- package/dist/TellegenProvider.svelte.d.ts.map +1 -0
- package/dist/TellegenShell.svelte +126 -0
- package/dist/TellegenShell.svelte.d.ts +4 -0
- package/dist/TellegenShell.svelte.d.ts.map +1 -0
- package/dist/TellegenViewer.svelte +19 -0
- package/dist/TellegenViewer.svelte.d.ts +4 -0
- package/dist/TellegenViewer.svelte.d.ts.map +1 -0
- package/dist/api.d.ts +44 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +137 -0
- package/dist/colors.d.ts +51 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +140 -0
- package/dist/components/AppFooter.svelte +73 -0
- package/dist/components/AppFooter.svelte.d.ts +19 -0
- package/dist/components/AppFooter.svelte.d.ts.map +1 -0
- package/dist/components/AppHeader.svelte +405 -0
- package/dist/components/AppHeader.svelte.d.ts +4 -0
- package/dist/components/AppHeader.svelte.d.ts.map +1 -0
- package/dist/components/BindingLines.svelte +101 -0
- package/dist/components/BindingLines.svelte.d.ts +4 -0
- package/dist/components/BindingLines.svelte.d.ts.map +1 -0
- package/dist/components/BusPicker.svelte +227 -0
- package/dist/components/BusPicker.svelte.d.ts +4 -0
- package/dist/components/BusPicker.svelte.d.ts.map +1 -0
- package/dist/components/ControlPanel.svelte +154 -0
- package/dist/components/ControlPanel.svelte.d.ts +19 -0
- package/dist/components/ControlPanel.svelte.d.ts.map +1 -0
- package/dist/components/DemandSlider.svelte +271 -0
- package/dist/components/DemandSlider.svelte.d.ts +4 -0
- package/dist/components/DemandSlider.svelte.d.ts.map +1 -0
- package/dist/components/DisplayControls.svelte +81 -0
- package/dist/components/DisplayControls.svelte.d.ts +19 -0
- package/dist/components/DisplayControls.svelte.d.ts.map +1 -0
- package/dist/components/DropZone.svelte +46 -0
- package/dist/components/DropZone.svelte.d.ts +19 -0
- package/dist/components/DropZone.svelte.d.ts.map +1 -0
- package/dist/components/FormulationSelector.svelte +82 -0
- package/dist/components/FormulationSelector.svelte.d.ts +19 -0
- package/dist/components/FormulationSelector.svelte.d.ts.map +1 -0
- package/dist/components/LocalCaseDetails.svelte +253 -0
- package/dist/components/LocalCaseDetails.svelte.d.ts +4 -0
- package/dist/components/LocalCaseDetails.svelte.d.ts.map +1 -0
- package/dist/components/MulticonductorDetails.svelte +280 -0
- package/dist/components/MulticonductorDetails.svelte.d.ts +19 -0
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +1 -0
- package/dist/components/NetworkStats.svelte +75 -0
- package/dist/components/NetworkStats.svelte.d.ts +19 -0
- package/dist/components/NetworkStats.svelte.d.ts.map +1 -0
- package/dist/components/PlacementCue.svelte +35 -0
- package/dist/components/PlacementCue.svelte.d.ts +19 -0
- package/dist/components/PlacementCue.svelte.d.ts.map +1 -0
- package/dist/components/RatingSlider.svelte +273 -0
- package/dist/components/RatingSlider.svelte.d.ts +4 -0
- package/dist/components/RatingSlider.svelte.d.ts.map +1 -0
- package/dist/components/RestoreDefaultsButton.svelte +34 -0
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +19 -0
- package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +1 -0
- package/dist/components/SensitivityReadout.svelte +126 -0
- package/dist/components/SensitivityReadout.svelte.d.ts +4 -0
- package/dist/components/SensitivityReadout.svelte.d.ts.map +1 -0
- package/dist/components/SizeLegend.svelte +55 -0
- package/dist/components/SizeLegend.svelte.d.ts +19 -0
- package/dist/components/SizeLegend.svelte.d.ts.map +1 -0
- package/dist/components/SolveCard.svelte +84 -0
- package/dist/components/SolveCard.svelte.d.ts +4 -0
- package/dist/components/SolveCard.svelte.d.ts.map +1 -0
- package/dist/components/TopMovers.svelte +68 -0
- package/dist/components/TopMovers.svelte.d.ts +19 -0
- package/dist/components/TopMovers.svelte.d.ts.map +1 -0
- package/dist/components.d.ts +19 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +18 -0
- package/dist/context.svelte.d.ts +20 -0
- package/dist/context.svelte.d.ts.map +1 -0
- package/dist/context.svelte.js +43 -0
- package/dist/controller.svelte.d.ts +334 -0
- package/dist/controller.svelte.d.ts.map +1 -0
- package/dist/controller.svelte.js +2026 -0
- package/dist/display.d.ts +31 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +63 -0
- package/dist/drop-classify.d.ts +48 -0
- package/dist/drop-classify.d.ts.map +1 -0
- package/dist/drop-classify.js +92 -0
- package/dist/format.d.ts +30 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +49 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/map.d.ts +9 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +4 -0
- package/dist/multiconductor.d.ts +63 -0
- package/dist/multiconductor.d.ts.map +1 -0
- package/dist/multiconductor.js +255 -0
- package/dist/state.svelte.d.ts +258 -0
- package/dist/state.svelte.d.ts.map +1 -0
- package/dist/state.svelte.js +323 -0
- package/dist/study-package.d.ts +9 -0
- package/dist/study-package.d.ts.map +1 -0
- package/dist/study-package.js +17 -0
- package/dist/styles.css +393 -0
- package/dist/synthetic-layout.d.ts +12 -0
- package/dist/synthetic-layout.d.ts.map +1 -0
- package/dist/synthetic-layout.js +271 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { DEFAULT_FORMULATION } from '@tellegen/engine';
|
|
2
|
+
/** A case is perturbed when any committed demand or rating delta is nonzero. Shared
|
|
3
|
+
* by both solvable case classes so the "perturbed" rule stays single-sourced. */
|
|
4
|
+
const hasPerturbation = (edits) => Object.values(edits).some((mw) => mw !== 0);
|
|
5
|
+
/** A case file parsed in the browser. Network cases can solve after they have
|
|
6
|
+
* coordinates; a .pwd display file has no case summary, substations only. A
|
|
7
|
+
* class with a stable identity and reactive fields, like CaseState: the solve
|
|
8
|
+
* and sensitivity flows mutate fields directly (each is $state, so the panel
|
|
9
|
+
* re-renders) and the seq tokens stay attached across overlapping async
|
|
10
|
+
* callbacks, so a stale solve can neither freeze the UI nor clobber a newer one. */
|
|
11
|
+
export class LocalCase {
|
|
12
|
+
id;
|
|
13
|
+
label;
|
|
14
|
+
fileName;
|
|
15
|
+
/** Case stats; null for a .pwd display only entry. */
|
|
16
|
+
summary = $state.raw(null);
|
|
17
|
+
/** Raw powerio Network JSON for the browser solver branch. */
|
|
18
|
+
networkJson = $state.raw(undefined);
|
|
19
|
+
/** Topology for synthetic placement when the file has no coordinates. */
|
|
20
|
+
topology = $state.raw(undefined);
|
|
21
|
+
coordsKind = $state.raw(undefined);
|
|
22
|
+
/** Map geometry when the file carried or received coordinates. */
|
|
23
|
+
view = $state.raw(null);
|
|
24
|
+
syntheticCenter = $state.raw(undefined);
|
|
25
|
+
geoSource = $state.raw(undefined);
|
|
26
|
+
geoWarnings = $state.raw(undefined);
|
|
27
|
+
network = $state.raw(null);
|
|
28
|
+
baseSolution = $state.raw(null);
|
|
29
|
+
solution = $state.raw(null);
|
|
30
|
+
sensitivity = $state.raw(null);
|
|
31
|
+
deltas = $state.raw({});
|
|
32
|
+
/** Committed branch rating deltas (MW from base, keyed by branch). */
|
|
33
|
+
ratings = $state.raw({});
|
|
34
|
+
/** The OPF formulation the browser Study solves for this case: DC OPF (default),
|
|
35
|
+
* full AC OPF, or the SOCWR relaxation. Changing it rebuilds the Study. */
|
|
36
|
+
formulation = $state(DEFAULT_FORMULATION);
|
|
37
|
+
iterations = $state.raw([]);
|
|
38
|
+
solving = $state(false);
|
|
39
|
+
solveMs = $state(null);
|
|
40
|
+
solveBackend = $state(null);
|
|
41
|
+
solveFallbackReason = $state(null);
|
|
42
|
+
/** Monotone token: only the latest solve may write this case. */
|
|
43
|
+
solveSeq = 0;
|
|
44
|
+
/** Monotone token: only the latest sensitivity request may write this case. */
|
|
45
|
+
sensitivitySeq = 0;
|
|
46
|
+
predictedObjective = $state(null);
|
|
47
|
+
/** Present for a PowerWorld .pwd display only entry. */
|
|
48
|
+
substations = $state.raw(undefined);
|
|
49
|
+
constructor(init) {
|
|
50
|
+
this.id = init.id;
|
|
51
|
+
this.label = init.label;
|
|
52
|
+
this.fileName = init.fileName;
|
|
53
|
+
this.summary = init.summary ?? null;
|
|
54
|
+
this.networkJson = init.networkJson;
|
|
55
|
+
this.topology = init.topology;
|
|
56
|
+
this.coordsKind = init.coordsKind;
|
|
57
|
+
this.view = init.view ?? null;
|
|
58
|
+
this.substations = init.substations;
|
|
59
|
+
}
|
|
60
|
+
get perturbed() {
|
|
61
|
+
return hasPerturbation(this.deltas) || hasPerturbation(this.ratings);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** One islanded network with its own solver state on the server. API
|
|
65
|
+
* payloads are reassigned wholesale, so $state.raw throughout. */
|
|
66
|
+
export class CaseState {
|
|
67
|
+
id;
|
|
68
|
+
name;
|
|
69
|
+
network = $state.raw(null);
|
|
70
|
+
/** Raw powerio Network JSON for the browser solver; fetched lazily. */
|
|
71
|
+
networkJson = $state.raw(null);
|
|
72
|
+
/** Boot solution at base demand; never changes. */
|
|
73
|
+
baseSolution = $state.raw(null);
|
|
74
|
+
/** Exact solution at the current committed perturbation. */
|
|
75
|
+
solution = $state.raw(null);
|
|
76
|
+
sensitivity = $state.raw(null);
|
|
77
|
+
/** Committed demand deltas (MW from base, keyed by bus). */
|
|
78
|
+
deltas = $state.raw({});
|
|
79
|
+
/** Committed branch rating deltas (MW from base, keyed by branch). */
|
|
80
|
+
ratings = $state.raw({});
|
|
81
|
+
/** The OPF formulation the browser Study solves for this case: DC OPF (default),
|
|
82
|
+
* full AC OPF, or the SOCWR relaxation. Changing it rebuilds the Study. */
|
|
83
|
+
formulation = $state(DEFAULT_FORMULATION);
|
|
84
|
+
iterations = $state.raw([]);
|
|
85
|
+
solving = $state(false);
|
|
86
|
+
solveMs = $state(null);
|
|
87
|
+
solveBackend = $state(null);
|
|
88
|
+
solveFallbackReason = $state(null);
|
|
89
|
+
/** Monotone token: only the latest solve may write this case. */
|
|
90
|
+
solveSeq = 0;
|
|
91
|
+
/** Monotone token: only the latest sensitivity request may write this case. */
|
|
92
|
+
sensitivitySeq = 0;
|
|
93
|
+
/** Closer for this case's in-flight server solve stream, if any. Owned per
|
|
94
|
+
* case so closing one case's stream never strands another's solve. */
|
|
95
|
+
closeStream = null;
|
|
96
|
+
/** Objective change the gradient predicted for the last commit, to score
|
|
97
|
+
* the preview once the exact solve lands. */
|
|
98
|
+
predictedObjective = $state(null);
|
|
99
|
+
constructor(summary) {
|
|
100
|
+
this.id = summary.id;
|
|
101
|
+
this.name = summary.name;
|
|
102
|
+
}
|
|
103
|
+
get perturbed() {
|
|
104
|
+
return hasPerturbation(this.deltas) || hasPerturbation(this.ratings);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** A multiconductor distribution case parsed in the browser, viewed only: no
|
|
108
|
+
* formulation, no sliders, no Study. It carries the bus/terminal graph, its
|
|
109
|
+
* placed map view, and the selected bus whose terminal detail expands. Fields
|
|
110
|
+
* are reactive like the solvable cases so the panel and map track them. */
|
|
111
|
+
export class MulticonductorCase {
|
|
112
|
+
id;
|
|
113
|
+
label;
|
|
114
|
+
fileName;
|
|
115
|
+
/** Summary counts and coordinate provenance from the parse. */
|
|
116
|
+
summary = $state.raw(null);
|
|
117
|
+
/** The render-ready bus/terminal graph. */
|
|
118
|
+
graph = $state.raw(null);
|
|
119
|
+
/** Placement kind resolved at ingest from the case's coordinate space. */
|
|
120
|
+
coordsKind = $state.raw('synthetic');
|
|
121
|
+
/** The placed map view; null until placed (planar/synthetic await a center). */
|
|
122
|
+
view = $state.raw(null);
|
|
123
|
+
syntheticCenter = $state.raw(undefined);
|
|
124
|
+
/** The selected bus id, whose terminal stack and incident conductors expand;
|
|
125
|
+
* null when nothing is selected. String-keyed: distribution bus ids are names. */
|
|
126
|
+
selectedBusId = $state(null);
|
|
127
|
+
constructor(init) {
|
|
128
|
+
this.id = init.id;
|
|
129
|
+
this.label = init.label;
|
|
130
|
+
this.fileName = init.fileName;
|
|
131
|
+
this.summary = init.summary;
|
|
132
|
+
this.graph = init.graph;
|
|
133
|
+
this.coordsKind = init.coordsKind;
|
|
134
|
+
this.view = init.view ?? null;
|
|
135
|
+
}
|
|
136
|
+
/** Whether the case is placed and ready to render. */
|
|
137
|
+
get placed() {
|
|
138
|
+
return this.view !== null;
|
|
139
|
+
}
|
|
140
|
+
/** The selected bus's placed detail, or null. */
|
|
141
|
+
get selectedBus() {
|
|
142
|
+
if (this.selectedBusId === null)
|
|
143
|
+
return null;
|
|
144
|
+
return this.view?.buses.find((b) => b.id === this.selectedBusId) ?? null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export class AppState {
|
|
148
|
+
cases = $state.raw([]);
|
|
149
|
+
activeCaseId = $state(null);
|
|
150
|
+
/** Selected bus in the active case. */
|
|
151
|
+
selectedBus = $state(null);
|
|
152
|
+
/** Selected branch in the active case; mutually exclusive with selectedBus. */
|
|
153
|
+
selectedBranch = $state(null);
|
|
154
|
+
/** Live slider value (MW from base) before commit; null when idle. */
|
|
155
|
+
previewDeltaMw = $state(null);
|
|
156
|
+
/** Live rating slider value (MW from base) before commit; null when idle. */
|
|
157
|
+
previewRatingMw = $state(null);
|
|
158
|
+
/** True while the demand control should keep the map in LMP preview mode. */
|
|
159
|
+
previewActive = $state(false);
|
|
160
|
+
/** Engine first-order LMP preview for the live drag: predicted change in LMP
|
|
161
|
+
* ($/MWh) per bus at the previewed edit, scoped to the case and selection
|
|
162
|
+
* target (bus or branch) it was computed for. Set by the Study path; null when
|
|
163
|
+
* no Study preview applies (the map then falls back to the JS
|
|
164
|
+
* sensitivity-times-step preview). Reassigned wholesale, so $state.raw. */
|
|
165
|
+
previewLmp = $state.raw(null);
|
|
166
|
+
demandRangeMode = $state('local');
|
|
167
|
+
displayMode = $state('lmp');
|
|
168
|
+
sensitivityLoading = $state(false);
|
|
169
|
+
#error = $state(null);
|
|
170
|
+
/** Re-runs the operation behind the current `error`, when one applies. Every
|
|
171
|
+
* write to `error` clears it, so a retry op can never outlive its message. */
|
|
172
|
+
errorRetry = $state.raw(null);
|
|
173
|
+
get error() {
|
|
174
|
+
return this.#error;
|
|
175
|
+
}
|
|
176
|
+
set error(message) {
|
|
177
|
+
this.#error = message;
|
|
178
|
+
this.errorRetry = null;
|
|
179
|
+
}
|
|
180
|
+
/** Case files parsed in the browser via the powerio wasm module. */
|
|
181
|
+
localCases = $state.raw([]);
|
|
182
|
+
/** Local case the panel shows; clicking a bundled case or a bus clears it. */
|
|
183
|
+
activeLocalId = $state(null);
|
|
184
|
+
placingLocalId = $state(null);
|
|
185
|
+
/** Multiconductor distribution cases parsed in the browser (viewing only). */
|
|
186
|
+
multiCases = $state.raw([]);
|
|
187
|
+
/** Multiconductor case the panel shows; mutually exclusive with the solvable
|
|
188
|
+
* active ids. */
|
|
189
|
+
activeMultiId = $state(null);
|
|
190
|
+
/** Multiconductor case awaiting placement on the map. */
|
|
191
|
+
placingMultiId = $state(null);
|
|
192
|
+
dragOver = $state(false);
|
|
193
|
+
parsingFile = $state(false);
|
|
194
|
+
/** Map framing request: bump seq so repeat targets still fly. `requestFrame`
|
|
195
|
+
* returns a promise the map resolves when the camera lands (or immediately
|
|
196
|
+
* when it cannot fly), so a caller can defer heavy work until the animation
|
|
197
|
+
* finishes. */
|
|
198
|
+
frameTarget = $state.raw('all');
|
|
199
|
+
frameSeq = $state(0);
|
|
200
|
+
#frameSettled = null;
|
|
201
|
+
get active() {
|
|
202
|
+
return this.cases.find((c) => c.id === this.activeCaseId) ?? null;
|
|
203
|
+
}
|
|
204
|
+
byId(id) {
|
|
205
|
+
return this.cases.find((c) => c.id === id) ?? null;
|
|
206
|
+
}
|
|
207
|
+
get activeLocal() {
|
|
208
|
+
return this.localCases.find((c) => c.id === this.activeLocalId) ?? null;
|
|
209
|
+
}
|
|
210
|
+
get activeMulti() {
|
|
211
|
+
return this.multiCases.find((c) => c.id === this.activeMultiId) ?? null;
|
|
212
|
+
}
|
|
213
|
+
/** Whether any case is awaiting a click-to-place on the map (local or multi). */
|
|
214
|
+
get placingId() {
|
|
215
|
+
return this.placingLocalId ?? this.placingMultiId;
|
|
216
|
+
}
|
|
217
|
+
addLocal(c) {
|
|
218
|
+
this.localCases = [...this.localCases, c];
|
|
219
|
+
this.activeLocalId = c.id;
|
|
220
|
+
this.placingLocalId = c.coordsKind === 'synthetic_pending' ? c.id : null;
|
|
221
|
+
}
|
|
222
|
+
/** Add a multiconductor case and make it active. A case that still needs a
|
|
223
|
+
* map center (planar/synthetic, no view yet) enters placement. */
|
|
224
|
+
addMulti(c) {
|
|
225
|
+
this.multiCases = [...this.multiCases, c];
|
|
226
|
+
this.activeCaseId = null;
|
|
227
|
+
this.activeLocalId = null;
|
|
228
|
+
this.activeMultiId = c.id;
|
|
229
|
+
this.placingLocalId = null;
|
|
230
|
+
this.placingMultiId = c.placed ? null : c.id;
|
|
231
|
+
}
|
|
232
|
+
removeMulti(id) {
|
|
233
|
+
const wasActive = this.activeMultiId === id;
|
|
234
|
+
this.multiCases = this.multiCases.filter((c) => c.id !== id);
|
|
235
|
+
if (this.placingMultiId === id)
|
|
236
|
+
this.placingMultiId = null;
|
|
237
|
+
if (!wasActive)
|
|
238
|
+
return { kind: 'none' };
|
|
239
|
+
this.activeMultiId = null;
|
|
240
|
+
if (this.activeCaseId !== null || this.activeLocalId !== null)
|
|
241
|
+
return { kind: 'none' };
|
|
242
|
+
return this.activateFallback();
|
|
243
|
+
}
|
|
244
|
+
removeCase(id) {
|
|
245
|
+
const wasActive = this.activeCaseId === id;
|
|
246
|
+
this.cases = this.cases.filter((c) => c.id !== id);
|
|
247
|
+
if (!wasActive)
|
|
248
|
+
return { kind: 'none' };
|
|
249
|
+
this.selectedBus = null;
|
|
250
|
+
this.selectedBranch = null;
|
|
251
|
+
this.previewDeltaMw = null;
|
|
252
|
+
this.previewRatingMw = null;
|
|
253
|
+
this.previewActive = false;
|
|
254
|
+
this.previewLmp = null;
|
|
255
|
+
this.demandRangeMode = 'local';
|
|
256
|
+
this.sensitivityLoading = false;
|
|
257
|
+
return this.activateFallback();
|
|
258
|
+
}
|
|
259
|
+
removeLocal(id) {
|
|
260
|
+
const wasActive = this.activeLocalId === id;
|
|
261
|
+
this.localCases = this.localCases.filter((c) => c.id !== id);
|
|
262
|
+
if (this.placingLocalId === id)
|
|
263
|
+
this.placingLocalId = null;
|
|
264
|
+
if (!wasActive)
|
|
265
|
+
return { kind: 'none' };
|
|
266
|
+
this.activeLocalId = null;
|
|
267
|
+
// A backend case can still be active; only pick a fallback when nothing is.
|
|
268
|
+
if (this.activeCaseId !== null)
|
|
269
|
+
return { kind: 'none' };
|
|
270
|
+
return this.activateFallback();
|
|
271
|
+
}
|
|
272
|
+
// Pick the next active case after a removal: the first remaining backend case,
|
|
273
|
+
// else a remaining local case that can render (a view or substations) or is
|
|
274
|
+
// awaiting placement, else the first remaining local. Frames whatever it picks.
|
|
275
|
+
activateFallback() {
|
|
276
|
+
this.activeCaseId = this.cases[0]?.id ?? null;
|
|
277
|
+
if (this.activeCaseId) {
|
|
278
|
+
this.requestFrame(this.activeCaseId);
|
|
279
|
+
return { kind: 'backend', id: this.activeCaseId };
|
|
280
|
+
}
|
|
281
|
+
const nextLocal = this.localCases.find((c) => c.view || c.substations || c.coordsKind === 'synthetic_pending') ??
|
|
282
|
+
this.localCases[0] ??
|
|
283
|
+
null;
|
|
284
|
+
if (nextLocal) {
|
|
285
|
+
this.activeLocalId = nextLocal.id;
|
|
286
|
+
this.placingLocalId = nextLocal.coordsKind === 'synthetic_pending' ? nextLocal.id : null;
|
|
287
|
+
if (nextLocal.view || nextLocal.substations)
|
|
288
|
+
this.requestFrame(nextLocal.id);
|
|
289
|
+
else
|
|
290
|
+
this.requestFrame('all');
|
|
291
|
+
return { kind: 'local', id: nextLocal.id };
|
|
292
|
+
}
|
|
293
|
+
this.activeLocalId = null;
|
|
294
|
+
this.placingLocalId = null;
|
|
295
|
+
// A multiconductor case is viewing only, so no hydration target: activate
|
|
296
|
+
// it, frame whatever is placed, and report `none`.
|
|
297
|
+
const nextMulti = this.multiCases[0] ?? null;
|
|
298
|
+
this.activeMultiId = nextMulti?.id ?? null;
|
|
299
|
+
this.placingMultiId = nextMulti && !nextMulti.placed ? nextMulti.id : null;
|
|
300
|
+
if (nextMulti?.placed)
|
|
301
|
+
this.requestFrame(nextMulti.id);
|
|
302
|
+
else
|
|
303
|
+
this.requestFrame('all');
|
|
304
|
+
return { kind: 'none' };
|
|
305
|
+
}
|
|
306
|
+
requestFrame(target) {
|
|
307
|
+
// A superseded request settles immediately; the new request owns the camera.
|
|
308
|
+
this.settleFrame();
|
|
309
|
+
this.frameTarget = target;
|
|
310
|
+
this.frameSeq++;
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
this.#frameSettled = resolve;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** The map calls this when the requested camera move has landed. */
|
|
316
|
+
settleFrame() {
|
|
317
|
+
this.#frameSettled?.();
|
|
318
|
+
this.#frameSettled = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export function createAppState() {
|
|
322
|
+
return new AppState();
|
|
323
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Client-side helper for saved study packages (`.pio.json`). The engine owns
|
|
2
|
+
* parsing, restoring, and exporting; classification of a dropped file lives in
|
|
3
|
+
* `drop-classify.ts` (the single owner). This module covers the one remaining
|
|
4
|
+
* frontend decision around a package: which commit an export targets. */
|
|
5
|
+
/** The commit index for a saved package's current committed state: its last study
|
|
6
|
+
* commit, or 0 when it carries none (export then writes the base network). Tolerant of
|
|
7
|
+
* malformed input, returning 0 rather than throwing. */
|
|
8
|
+
export declare function studyCommitIndex(packageJson: string): number;
|
|
9
|
+
//# sourceMappingURL=study-package.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"study-package.d.ts","sourceRoot":"","sources":["../src/lib/study-package.ts"],"names":[],"mappings":"AAAA;;;yEAGyE;AAEzE;;wDAEwD;AACxD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ5D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Client-side helper for saved study packages (`.pio.json`). The engine owns
|
|
2
|
+
* parsing, restoring, and exporting; classification of a dropped file lives in
|
|
3
|
+
* `drop-classify.ts` (the single owner). This module covers the one remaining
|
|
4
|
+
* frontend decision around a package: which commit an export targets. */
|
|
5
|
+
/** The commit index for a saved package's current committed state: its last study
|
|
6
|
+
* commit, or 0 when it carries none (export then writes the base network). Tolerant of
|
|
7
|
+
* malformed input, returning 0 rather than throwing. */
|
|
8
|
+
export function studyCommitIndex(packageJson) {
|
|
9
|
+
try {
|
|
10
|
+
const commits = JSON.parse(packageJson).study?.commits
|
|
11
|
+
?.length;
|
|
12
|
+
return Math.max(0, (commits ?? 0) - 1);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
}
|