seqeyes-python 0.3.9__tar.gz → 0.3.10__tar.gz
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.
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/PKG-INFO +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/pyproject.toml +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/resources/pulseq-bundle.js +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/resources/seqeyes-labels.js +20 -4
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/resources/webview-bundle.js +490 -54
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/.gitignore +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/README.md +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/examples/demo.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.10}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqeyes-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.10"
|
|
8
8
|
description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -7,13 +7,29 @@
|
|
|
7
7
|
the 2-D context, the time→x mapping and the row geometry. The Python
|
|
8
8
|
viewer loads this file on its own, without the rest of the bundle.
|
|
9
9
|
|
|
10
|
-
The DOM and
|
|
11
|
-
|
|
10
|
+
The DOM and storage are only touched inside functions, so the tests can
|
|
11
|
+
run the shipped file in a bare VM context.
|
|
12
12
|
|
|
13
13
|
A "labels" argument is anything with `names` and `kinds`: the popup and the
|
|
14
14
|
styles need nothing more, so ⚙ works before the per-ADC values arrive. */
|
|
15
15
|
var SeqEyesLabels = (function () {
|
|
16
16
|
var STORAGE_KEY = 'seqeyes.labelStyles.v1';
|
|
17
|
+
|
|
18
|
+
/* The Python viewer inlines this file on its own, so the bundle's
|
|
19
|
+
preferences gateway may not be there. Go through it when it is, so the
|
|
20
|
+
settings popover's master switch and Forget button cover marker styles
|
|
21
|
+
too, and fall back to raw storage when it is not. */
|
|
22
|
+
function prefsApi() { return (typeof SeqEyesPrefs !== 'undefined') ? SeqEyesPrefs : null; }
|
|
23
|
+
function readStored(key) {
|
|
24
|
+
var api = prefsApi();
|
|
25
|
+
if (api) return api.get(key);
|
|
26
|
+
try { return localStorage.getItem(key); } catch (_) { return null; }
|
|
27
|
+
}
|
|
28
|
+
function writeStored(key, value) {
|
|
29
|
+
var api = prefsApi();
|
|
30
|
+
if (api) { api.set(key, value); return; }
|
|
31
|
+
try { localStorage.setItem(key, value); } catch (_) {}
|
|
32
|
+
}
|
|
17
33
|
// Tableau 10: distinct hues that stay readable on light and dark themes.
|
|
18
34
|
var PALETTE = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
|
19
35
|
var SHAPES = ['circle', 'square', 'triangle', 'triangleDown', 'diamond', 'cross', 'plus'];
|
|
@@ -74,14 +90,14 @@ var SeqEyesLabels = (function () {
|
|
|
74
90
|
if (overrides) return overrides;
|
|
75
91
|
overrides = {};
|
|
76
92
|
try {
|
|
77
|
-
var saved = JSON.parse(
|
|
93
|
+
var saved = JSON.parse(readStored(STORAGE_KEY) || '{}');
|
|
78
94
|
if (saved && typeof saved === 'object') overrides = saved;
|
|
79
95
|
} catch (_) {}
|
|
80
96
|
return overrides;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
function writeOverrides() {
|
|
84
|
-
|
|
100
|
+
writeStored(STORAGE_KEY, JSON.stringify(overrides));
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
/* Defaults follow the label's position in its sequence, so one sequence's
|
|
@@ -1,5 +1,205 @@
|
|
|
1
1
|
/* SeqEyes WebView Bundle — auto-generated, do not edit */
|
|
2
2
|
"use strict";
|
|
3
|
+
/* ══ prefs.js ══ */
|
|
4
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
5
|
+
Viewer preferences — one consent-gated gateway to localStorage
|
|
6
|
+
|
|
7
|
+
Every host (VS Code webview, standalone web page, MATLAB) keeps its
|
|
8
|
+
remembered settings under the `seqeyes.` prefix. Before this module there
|
|
9
|
+
were three near-identical copies of the get/set pair — kspace.js, panel.js
|
|
10
|
+
and the inline script in web/index.html — and none of them asked whether
|
|
11
|
+
the user wanted anything written down at all.
|
|
12
|
+
|
|
13
|
+
Two rules shape the API:
|
|
14
|
+
|
|
15
|
+
- Persistence is opt-out, not unconditional. `seqeyes.rememberSettings`
|
|
16
|
+
is the master switch. With it off, `set()` keeps the value in memory
|
|
17
|
+
so the control still behaves for the rest of the session and nothing
|
|
18
|
+
reaches the disk; turning it off purges every key this module owns.
|
|
19
|
+
|
|
20
|
+
- localStorage is only ever touched inside a function, so the file loads
|
|
21
|
+
in a bare `node:vm` context for the unit tests the same way labels.js
|
|
22
|
+
does. Nothing here runs at load time.
|
|
23
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
24
|
+
|
|
25
|
+
var SeqEyesPrefs = (function () {
|
|
26
|
+
var PREFIX = 'seqeyes.';
|
|
27
|
+
|
|
28
|
+
/* Named so the standalone page's inline fork spells the keys the same way
|
|
29
|
+
the bundle does; a typo there would look like "my settings reset again". */
|
|
30
|
+
var KEYS = {
|
|
31
|
+
remember: 'seqeyes.rememberSettings',
|
|
32
|
+
theme: 'seqeyes.theme',
|
|
33
|
+
timeUnit: 'seqeyes.timeUnit',
|
|
34
|
+
gradUnit: 'seqeyes.gradUnit',
|
|
35
|
+
showBlocks: 'seqeyes.showBlocks',
|
|
36
|
+
panelWidth: 'seqeyes.panelWidth',
|
|
37
|
+
panelHeight: 'seqeyes.panelHeight',
|
|
38
|
+
noticesCollapsed: 'seqeyes.viewerNoticesCollapsed',
|
|
39
|
+
kspaceUnit: 'seqeyes.kspace.unit',
|
|
40
|
+
kspaceDotSize: 'seqeyes.kspace.dotSize',
|
|
41
|
+
kspaceProjection: 'seqeyes.kspace.projection',
|
|
42
|
+
ascName: 'seqeyes.asc.name',
|
|
43
|
+
ascText: 'seqeyes.asc.text'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* A profile bigger than this is not worth fighting the quota over: the
|
|
47
|
+
whole origin gets about 5 MB, and a real Siemens gradient ASC is tens of
|
|
48
|
+
kilobytes. Anything this large means the picker found the wrong file. */
|
|
49
|
+
var ASC_MAX_CHARS = 2 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
/* Values written while persistence is off, or refused by a full quota.
|
|
52
|
+
Keeping them is what lets someone switch units, decide they do want the
|
|
53
|
+
choice kept, and tick the box without redoing the work. */
|
|
54
|
+
var session = {};
|
|
55
|
+
|
|
56
|
+
function store() {
|
|
57
|
+
try { return (typeof localStorage !== 'undefined') ? localStorage : null; } catch (e) { return null; }
|
|
58
|
+
}
|
|
59
|
+
function rawGet(key) {
|
|
60
|
+
var s = store(); if (!s) return null;
|
|
61
|
+
try { return s.getItem(key); } catch (e) { return null; }
|
|
62
|
+
}
|
|
63
|
+
function rawSet(key, value) {
|
|
64
|
+
var s = store(); if (!s) return false;
|
|
65
|
+
try { s.setItem(key, value); return true; } catch (e) { return false; }
|
|
66
|
+
}
|
|
67
|
+
function rawRemove(key) {
|
|
68
|
+
var s = store(); if (!s) return;
|
|
69
|
+
try { s.removeItem(key); } catch (e) { /* private mode */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Default on. localStorage was already load-bearing here — theme, panel
|
|
73
|
+
size and every spectrogram parameter — so defaulting off would quietly
|
|
74
|
+
take away persistence people are already relying on. */
|
|
75
|
+
function enabled() { return rawGet(KEYS.remember) !== '0'; }
|
|
76
|
+
|
|
77
|
+
function get(key) {
|
|
78
|
+
if (enabled()) {
|
|
79
|
+
var stored = rawGet(key);
|
|
80
|
+
if (stored !== null) return stored;
|
|
81
|
+
}
|
|
82
|
+
return Object.prototype.hasOwnProperty.call(session, key) ? session[key] : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Returns whether the value reached the disk, which setAsc uses to decide
|
|
86
|
+
whether a restore will actually be possible next time.
|
|
87
|
+
|
|
88
|
+
A successful write drops any session copy instead of mirroring it: the
|
|
89
|
+
disk is then the single source of truth, so a key that later goes missing
|
|
90
|
+
reads as missing rather than being answered from a stale shadow. */
|
|
91
|
+
function set(key, value) {
|
|
92
|
+
var text = String(value);
|
|
93
|
+
if (enabled() && rawSet(key, text)) { delete session[key]; return true; }
|
|
94
|
+
session[key] = text;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function remove(key) {
|
|
99
|
+
delete session[key];
|
|
100
|
+
rawRemove(key);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getNum(key, fallback) {
|
|
104
|
+
var raw = get(key);
|
|
105
|
+
var value = raw === null ? NaN : parseFloat(raw);
|
|
106
|
+
return isFinite(value) ? value : fallback;
|
|
107
|
+
}
|
|
108
|
+
function getBool(key, fallback) {
|
|
109
|
+
var raw = get(key);
|
|
110
|
+
return raw === null ? !!fallback : raw === '1';
|
|
111
|
+
}
|
|
112
|
+
function setBool(key, value) { return set(key, value ? '1' : '0'); }
|
|
113
|
+
|
|
114
|
+
/* The guard that keeps a stale or hand-edited key from putting the viewer
|
|
115
|
+
into a state its own controls cannot express. */
|
|
116
|
+
function getEnum(key, allowed, fallback) {
|
|
117
|
+
var raw = get(key);
|
|
118
|
+
return (raw !== null && allowed.indexOf(raw) >= 0) ? raw : fallback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Every key this module owns, minus the master switch: a "no" has to
|
|
122
|
+
survive the purge it triggers. */
|
|
123
|
+
function storedKeys() {
|
|
124
|
+
var s = store(); if (!s) return [];
|
|
125
|
+
var found = [];
|
|
126
|
+
try {
|
|
127
|
+
for (var i = 0; i < s.length; i++) {
|
|
128
|
+
var key = s.key(i);
|
|
129
|
+
if (key && key.indexOf(PREFIX) === 0 && key !== KEYS.remember) found.push(key);
|
|
130
|
+
}
|
|
131
|
+
} catch (e) { return []; }
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clearStored() {
|
|
136
|
+
var doomed = storedKeys();
|
|
137
|
+
for (var i = 0; i < doomed.length; i++) rawRemove(doomed[i]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* "Forget stored settings": wipe the disk and the in-memory shadow, so the
|
|
141
|
+
next reload really does start from defaults. */
|
|
142
|
+
function forget() {
|
|
143
|
+
session = {};
|
|
144
|
+
clearStored();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function setEnabled(on) {
|
|
148
|
+
if (on) {
|
|
149
|
+
rawSet(KEYS.remember, '1');
|
|
150
|
+
/* Write through whatever the session accumulated while we were not
|
|
151
|
+
allowed to, so ticking the box keeps the state now on screen. */
|
|
152
|
+
var keys = Object.keys(session);
|
|
153
|
+
for (var i = 0; i < keys.length; i++) rawSet(keys[i], session[keys[i]]);
|
|
154
|
+
} else {
|
|
155
|
+
clearStored();
|
|
156
|
+
rawSet(KEYS.remember, '0');
|
|
157
|
+
}
|
|
158
|
+
return enabled();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── ASC profile cache (standalone web and MATLAB hosts) ───────────────
|
|
162
|
+
The VS Code host stores a path on the extension side instead; a browser
|
|
163
|
+
file input hands over a File with no re-openable path, so the text is
|
|
164
|
+
the only thing worth keeping. */
|
|
165
|
+
function setAsc(name, text) {
|
|
166
|
+
if (!name || typeof text !== 'string' || text.length > ASC_MAX_CHARS) { clearAsc(); return false; }
|
|
167
|
+
var ok = set(KEYS.ascText, text);
|
|
168
|
+
set(KEYS.ascName, name);
|
|
169
|
+
return ok;
|
|
170
|
+
}
|
|
171
|
+
function getAsc() {
|
|
172
|
+
var name = get(KEYS.ascName), text = get(KEYS.ascText);
|
|
173
|
+
/* Both or nothing: a half-written pair (quota hit between the two sets)
|
|
174
|
+
must not restore a button label for a profile we cannot re-parse. */
|
|
175
|
+
return (name && text) ? { name: name, text: text } : null;
|
|
176
|
+
}
|
|
177
|
+
function clearAsc() {
|
|
178
|
+
remove(KEYS.ascName);
|
|
179
|
+
remove(KEYS.ascText);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
KEYS: KEYS,
|
|
184
|
+
ASC_MAX_CHARS: ASC_MAX_CHARS,
|
|
185
|
+
enabled: enabled,
|
|
186
|
+
setEnabled: setEnabled,
|
|
187
|
+
get: get,
|
|
188
|
+
set: set,
|
|
189
|
+
remove: remove,
|
|
190
|
+
getNum: getNum,
|
|
191
|
+
getBool: getBool,
|
|
192
|
+
setBool: setBool,
|
|
193
|
+
getEnum: getEnum,
|
|
194
|
+
storedKeys: storedKeys,
|
|
195
|
+
forget: forget,
|
|
196
|
+
setAsc: setAsc,
|
|
197
|
+
getAsc: getAsc,
|
|
198
|
+
clearAsc: clearAsc
|
|
199
|
+
};
|
|
200
|
+
})();
|
|
201
|
+
|
|
202
|
+
|
|
3
203
|
/* ══ block-transport.js ══ */
|
|
4
204
|
/* ══════════════════════════════════════════════════════════════════════════
|
|
5
205
|
block-transport.js — rehydrate the packed block payload from the extension.
|
|
@@ -361,13 +561,29 @@ function failWaveformDetail(payload){
|
|
|
361
561
|
the 2-D context, the time→x mapping and the row geometry. The Python
|
|
362
562
|
viewer loads this file on its own, without the rest of the bundle.
|
|
363
563
|
|
|
364
|
-
The DOM and
|
|
365
|
-
|
|
564
|
+
The DOM and storage are only touched inside functions, so the tests can
|
|
565
|
+
run the shipped file in a bare VM context.
|
|
366
566
|
|
|
367
567
|
A "labels" argument is anything with `names` and `kinds`: the popup and the
|
|
368
568
|
styles need nothing more, so ⚙ works before the per-ADC values arrive. */
|
|
369
569
|
var SeqEyesLabels = (function () {
|
|
370
570
|
var STORAGE_KEY = 'seqeyes.labelStyles.v1';
|
|
571
|
+
|
|
572
|
+
/* The Python viewer inlines this file on its own, so the bundle's
|
|
573
|
+
preferences gateway may not be there. Go through it when it is, so the
|
|
574
|
+
settings popover's master switch and Forget button cover marker styles
|
|
575
|
+
too, and fall back to raw storage when it is not. */
|
|
576
|
+
function prefsApi() { return (typeof SeqEyesPrefs !== 'undefined') ? SeqEyesPrefs : null; }
|
|
577
|
+
function readStored(key) {
|
|
578
|
+
var api = prefsApi();
|
|
579
|
+
if (api) return api.get(key);
|
|
580
|
+
try { return localStorage.getItem(key); } catch (_) { return null; }
|
|
581
|
+
}
|
|
582
|
+
function writeStored(key, value) {
|
|
583
|
+
var api = prefsApi();
|
|
584
|
+
if (api) { api.set(key, value); return; }
|
|
585
|
+
try { localStorage.setItem(key, value); } catch (_) {}
|
|
586
|
+
}
|
|
371
587
|
// Tableau 10: distinct hues that stay readable on light and dark themes.
|
|
372
588
|
var PALETTE = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
|
373
589
|
var SHAPES = ['circle', 'square', 'triangle', 'triangleDown', 'diamond', 'cross', 'plus'];
|
|
@@ -428,14 +644,14 @@ var SeqEyesLabels = (function () {
|
|
|
428
644
|
if (overrides) return overrides;
|
|
429
645
|
overrides = {};
|
|
430
646
|
try {
|
|
431
|
-
var saved = JSON.parse(
|
|
647
|
+
var saved = JSON.parse(readStored(STORAGE_KEY) || '{}');
|
|
432
648
|
if (saved && typeof saved === 'object') overrides = saved;
|
|
433
649
|
} catch (_) {}
|
|
434
650
|
return overrides;
|
|
435
651
|
}
|
|
436
652
|
|
|
437
653
|
function writeOverrides() {
|
|
438
|
-
|
|
654
|
+
writeStored(STORAGE_KEY, JSON.stringify(overrides));
|
|
439
655
|
}
|
|
440
656
|
|
|
441
657
|
/* Defaults follow the label's position in its sequence, so one sequence's
|
|
@@ -808,8 +1024,13 @@ var CH_ORDER=[0,1,2,3,4,5,6,11,7,8,9,10];
|
|
|
808
1024
|
var ox=0,sc=1; // view offset [s] & scale [px/s]
|
|
809
1025
|
var dr=false,dsx=0,dso=0; // drag state
|
|
810
1026
|
var cursorT=0,cursorActive=false; // mouse time position
|
|
811
|
-
|
|
812
|
-
|
|
1027
|
+
/* Validating a remembered unit against the dropdown it came from, rather than
|
|
1028
|
+
against a second hard-coded list, keeps a stale or hand-edited key from
|
|
1029
|
+
putting the viewer in a state its own toolbar cannot express. */
|
|
1030
|
+
function selectOptionValues(sel){var out=[],i;if(!sel)return out;for(i=0;i<sel.options.length;i++)out.push(sel.options[i].value);return out;}
|
|
1031
|
+
var timeUnit=SeqEyesPrefs.getEnum(SeqEyesPrefs.KEYS.timeUnit,selectOptionValues(tuSel),'ms'),
|
|
1032
|
+
gradUnit=SeqEyesPrefs.getEnum(SeqEyesPrefs.KEYS.gradUnit,selectOptionValues(guSel),'Hz/m');
|
|
1033
|
+
var showBB=SeqEyesPrefs.getBool(SeqEyesPrefs.KEYS.showBlocks,false); // show block boundaries
|
|
813
1034
|
var GAMMA=42576; // Hz/m per mT/m for 1H
|
|
814
1035
|
|
|
815
1036
|
var ampZoom=[1,1,1,1,1,1,1];
|
|
@@ -830,10 +1051,10 @@ function viewerNoticeMessages(value){
|
|
|
830
1051
|
for(var i=0;i<values.length;i++)if(values[i])messages.push(String(values[i]));
|
|
831
1052
|
return messages;
|
|
832
1053
|
}
|
|
833
|
-
function readViewerNoticesCollapsed(){
|
|
1054
|
+
function readViewerNoticesCollapsed(){var saved=SeqEyesPrefs.get(SeqEyesPrefs.KEYS.noticesCollapsed);return saved===null?isMobileSafetyLayout():saved==='1';}
|
|
834
1055
|
function setViewerNoticesCollapsed(collapsed){
|
|
835
1056
|
viewerNoticesCollapsed=!!collapsed;
|
|
836
|
-
|
|
1057
|
+
SeqEyesPrefs.setBool(SeqEyesPrefs.KEYS.noticesCollapsed,viewerNoticesCollapsed);
|
|
837
1058
|
renderViewerNotices();
|
|
838
1059
|
}
|
|
839
1060
|
function renderViewerNotices(){
|
|
@@ -1021,9 +1242,9 @@ function applyLayoutMode(){
|
|
|
1021
1242
|
}
|
|
1022
1243
|
}
|
|
1023
1244
|
}
|
|
1024
|
-
function panelStoredWidth(){
|
|
1245
|
+
function panelStoredWidth(){return SeqEyesPrefs.getNum(SeqEyesPrefs.KEYS.panelWidth,500);}
|
|
1025
1246
|
function panelMaxHeight(){var main=document.getElementById('main'),available=main?main.getBoundingClientRect().height:window.innerHeight;return Math.max(100,available-120);}
|
|
1026
|
-
function panelStoredHeight(){var available=document.getElementById('main'),height=available?available.getBoundingClientRect().height:window.innerHeight,
|
|
1247
|
+
function panelStoredHeight(){var available=document.getElementById('main'),height=available?available.getBoundingClientRect().height:window.innerHeight,max=panelMaxHeight(),min=Math.min(180,max);var raw=SeqEyesPrefs.getNum(SeqEyesPrefs.KEYS.panelHeight,isMobileSafetyLayout()?height*.58:300);return Math.max(min,Math.min(raw,max));}
|
|
1027
1248
|
function refreshLayout(){
|
|
1028
1249
|
var changed=detectLayoutMode();
|
|
1029
1250
|
if(changed){
|
|
@@ -1079,8 +1300,9 @@ function buildLegend(){
|
|
|
1079
1300
|
});
|
|
1080
1301
|
}
|
|
1081
1302
|
buildLegend();
|
|
1082
|
-
tuSel.
|
|
1083
|
-
|
|
1303
|
+
tuSel.value=timeUnit;guSel.value=gradUnit;
|
|
1304
|
+
tuSel.onchange=function(){timeUnit=tuSel.value;SeqEyesPrefs.set(SeqEyesPrefs.KEYS.timeUnit,timeUnit);draw();};
|
|
1305
|
+
guSel.onchange=function(){gradUnit=guSel.value;SeqEyesPrefs.set(SeqEyesPrefs.KEYS.gradUnit,gradUnit);draw();};
|
|
1084
1306
|
function setExportButtonEnabled(enabled){if(exportBtn)exportBtn.disabled=!enabled;}
|
|
1085
1307
|
/* Throws rather than drawing an empty panel when a trajectory arrives without
|
|
1086
1308
|
its ADC samples. A reply that lost its arrays in transit used to clear the
|
|
@@ -1166,6 +1388,12 @@ window.addEventListener('message',function(e){
|
|
|
1166
1388
|
draw();drawKs();drawMinimap();
|
|
1167
1389
|
setExportButtonEnabled(true);
|
|
1168
1390
|
SeqEyesPanel.onSequenceLoaded();
|
|
1391
|
+
/* Ask the host to re-apply the ASC profile it remembered from an earlier
|
|
1392
|
+
session. Sent only now because PNS needs the sequence's blocks, and
|
|
1393
|
+
gated on the settings switch so one "no" covers both hosts. The
|
|
1394
|
+
extension ignores it once a profile is loaded, so a second sequence in
|
|
1395
|
+
the same tab costs nothing. */
|
|
1396
|
+
if(vscApi&&SeqEyesPrefs.enabled())vscApi.postMessage({command:'restoreAsc'});
|
|
1169
1397
|
requestAnimationFrame(function(){refreshLayout();draw();drawKs();drawMinimap();});
|
|
1170
1398
|
}else if(m.type==='waveformDetailData'){
|
|
1171
1399
|
var detailBlocks;
|
|
@@ -2773,7 +3001,16 @@ function drawTriggerBlocks(start,end,vi,ch,colors,vs,ve){
|
|
|
2773
3001
|
|
|
2774
3002
|
|
|
2775
3003
|
/* ══ kspace.js ══ */
|
|
2776
|
-
|
|
3004
|
+
/* Camera presets, declared first because the remembered projection is
|
|
3005
|
+
validated against this list and seeds the opening rotation. */
|
|
3006
|
+
var K_VIEWS=["3d","xy","xz","yz"];
|
|
3007
|
+
function kProjectionAngles(view){
|
|
3008
|
+
if(view==="xy")return{x:0,y:0};
|
|
3009
|
+
if(view==="xz")return{x:-Math.PI/2,y:0};
|
|
3010
|
+
if(view==="yz")return{x:0,y:Math.PI/2};
|
|
3011
|
+
return{x:-0.5,y:0.7}; // 3d — default perspective
|
|
3012
|
+
}
|
|
3013
|
+
var kOpen=false, kView=SeqEyesPrefs.getEnum(SeqEyesPrefs.KEYS.kspaceProjection,K_VIEWS,"3d");
|
|
2777
3014
|
var kSpaceTrajectoryDrawCount=0,kSpaceOverlayDrawCount=0;
|
|
2778
3015
|
/**
|
|
2779
3016
|
* Pan is a screen offset in CSS pixels, applied after rotation; the rotation
|
|
@@ -2787,7 +3024,8 @@ var kSpaceTrajectoryDrawCount=0,kSpaceOverlayDrawCount=0;
|
|
|
2787
3024
|
*/
|
|
2788
3025
|
var kPanX=0, kPanY=0, kScl=1;
|
|
2789
3026
|
var kAutoFit=true;
|
|
2790
|
-
var
|
|
3027
|
+
var _kOpeningAngles=kProjectionAngles(kView);
|
|
3028
|
+
var kRotX=_kOpeningAngles.x, kRotY=_kOpeningAngles.y; // matches the restored projection
|
|
2791
3029
|
var kDragging=false, kDragPrev=null, kDragBtn=0;
|
|
2792
3030
|
/**
|
|
2793
3031
|
* Pending coalesced drag redraw. A drag updates the rotation on every
|
|
@@ -2797,7 +3035,10 @@ var kDragging=false, kDragPrev=null, kDragBtn=0;
|
|
|
2797
3035
|
*/
|
|
2798
3036
|
var _kDragRaf=0;
|
|
2799
3037
|
var kCanvas=document.getElementById("kc"), kCtx=kCanvas.getContext("2d");
|
|
2800
|
-
|
|
3038
|
+
/* Clamped to the slider's own range so a hand-edited key cannot produce a dot
|
|
3039
|
+
size the control has no position for. */
|
|
3040
|
+
var kDotSize=Math.max(1,Math.min(12,Math.round(SeqEyesPrefs.getNum(SeqEyesPrefs.KEYS.kspaceDotSize,2)))),
|
|
3041
|
+
kUnit=SeqEyesPrefs.getEnum(SeqEyesPrefs.KEYS.kspaceUnit,["cyc","rad"],"cyc"); // cyc=1/m, rad=rad/m
|
|
2801
3042
|
|
|
2802
3043
|
// ── Smooth animation targets ──────────────────────────────────────────
|
|
2803
3044
|
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl, _tPanX=kPanX, _tPanY=kPanY;
|
|
@@ -2836,17 +3077,22 @@ function setKSpaceTarget(rx, ry, s, panX, panY, instant) {
|
|
|
2836
3077
|
startKSpaceAnim();
|
|
2837
3078
|
}
|
|
2838
3079
|
}
|
|
2839
|
-
|
|
2840
|
-
document.getElementById("
|
|
2841
|
-
|
|
3080
|
+
function kUnitLabel(){return kUnit==="cyc"?"Unit: 1/m":"Unit: rad/m";}
|
|
3081
|
+
var kDotSlider=document.getElementById("kdot"),kUnitBtn=document.getElementById("kunit");
|
|
3082
|
+
kDotSlider.value=String(kDotSize);
|
|
3083
|
+
kUnitBtn.textContent=kUnitLabel();
|
|
3084
|
+
kDotSlider.oninput=function(){
|
|
3085
|
+
kDotSize=parseInt(this.value);SeqEyesPrefs.set(SeqEyesPrefs.KEYS.kspaceDotSize,kDotSize);drawKs();
|
|
3086
|
+
};
|
|
3087
|
+
kUnitBtn.onclick=function(){
|
|
3088
|
+
kUnit=kUnit==="cyc"?"rad":"cyc";this.textContent=kUnitLabel();
|
|
3089
|
+
SeqEyesPrefs.set(SeqEyesPrefs.KEYS.kspaceUnit,kUnit);drawKs();
|
|
2842
3090
|
};
|
|
2843
3091
|
|
|
2844
3092
|
/* ── Theme selector (toolbar) ─────────────────────────────────────── */
|
|
2845
3093
|
var themeSelect=document.getElementById("theme");
|
|
2846
3094
|
var systemThemeQuery=(typeof window.matchMedia==="function")?window.matchMedia("(prefers-color-scheme: dark)"):null;
|
|
2847
3095
|
var inVsCode=!!vscApi;
|
|
2848
|
-
function storageGet(k){try{return localStorage.getItem(k);}catch(_){return null;}}
|
|
2849
|
-
function storageSet(k,v){try{localStorage.setItem(k,v);}catch(_){}}
|
|
2850
3096
|
function clearThemeClasses(){
|
|
2851
3097
|
var b=document.body,rm=[];
|
|
2852
3098
|
b.classList.forEach(function(c){if(c.indexOf("theme-")===0)rm.push(c);});
|
|
@@ -2866,11 +3112,11 @@ function applyThemeChoice(value,persist){
|
|
|
2866
3112
|
document.body.classList.add(systemThemeQuery&&systemThemeQuery.matches?"theme-github":"theme-githublight");
|
|
2867
3113
|
}
|
|
2868
3114
|
if(themeSelect&&themeSelect.value!==value)themeSelect.value=value;
|
|
2869
|
-
if(persist)
|
|
3115
|
+
if(persist)SeqEyesPrefs.set(SeqEyesPrefs.KEYS.theme,value);
|
|
2870
3116
|
redrawAfterThemeChange();
|
|
2871
3117
|
}
|
|
2872
3118
|
if(themeSelect){
|
|
2873
|
-
var savedTheme=
|
|
3119
|
+
var savedTheme=SeqEyesPrefs.get(SeqEyesPrefs.KEYS.theme)||"system";
|
|
2874
3120
|
if(!themeSelect.querySelector('option[value="'+savedTheme+'"]'))savedTheme="system";
|
|
2875
3121
|
themeSelect.onchange=function(){applyThemeChoice(this.value,true);};
|
|
2876
3122
|
applyThemeChoice(savedTheme,false);
|
|
@@ -3072,25 +3318,28 @@ function uploadKSpaceGPU(){
|
|
|
3072
3318
|
off -> k-space -> spectrogram -> off, and only the off -> k-space
|
|
3073
3319
|
transition passes through the safety gate. `kOpen` is still the flag the
|
|
3074
3320
|
drawing and interaction code reads, and panel.js keeps it in sync. */
|
|
3075
|
-
|
|
3076
|
-
|
|
3321
|
+
/* A free rotation is no longer any of the axis presets. Guarded on the
|
|
3322
|
+
current value so the drag handlers that call it on every move write once. */
|
|
3323
|
+
function kLeavePreset(){
|
|
3324
|
+
if(kView==="3d")return;
|
|
3077
3325
|
kView="3d";
|
|
3078
3326
|
document.getElementById("kax").textContent="3D";
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3327
|
+
SeqEyesPrefs.set(SeqEyesPrefs.KEYS.kspaceProjection,"3d");
|
|
3328
|
+
}
|
|
3329
|
+
/* One place that moves the camera to a named projection, so the reset button,
|
|
3330
|
+
the cycle button and the restore path cannot drift apart on the angles. */
|
|
3331
|
+
function applyKProjection(view,persist){
|
|
3332
|
+
kView=view;
|
|
3333
|
+
var angles=kProjectionAngles(view),af=_kAutoFitVals();
|
|
3334
|
+
document.getElementById("kax").textContent=view.toUpperCase();
|
|
3335
|
+
if(persist)SeqEyesPrefs.set(SeqEyesPrefs.KEYS.kspaceProjection,view);
|
|
3336
|
+
setKSpaceTarget(angles.x, angles.y, af.scl, af.panX, af.panY, false);
|
|
3337
|
+
}
|
|
3338
|
+
document.getElementById("kax").textContent=kView.toUpperCase();
|
|
3339
|
+
document.getElementById("krst").onclick=function(){applyKProjection("3d",true);};
|
|
3082
3340
|
// Camera presets: smoothly rotate to look straight down an axis
|
|
3083
3341
|
document.getElementById("kax").onclick=function(){
|
|
3084
|
-
|
|
3085
|
-
kView=views[(idx+1)%4];
|
|
3086
|
-
var trx=_tRotX, tr=_tRotY;
|
|
3087
|
-
if(kView==="xy"){trx=0; tr=0;}
|
|
3088
|
-
else if(kView==="xz"){trx=-Math.PI/2; tr=0;}
|
|
3089
|
-
else if(kView==="yz"){trx=0; tr=Math.PI/2;}
|
|
3090
|
-
else{trx=-0.5; tr=0.7;} // 3d — default perspective
|
|
3091
|
-
document.getElementById("kax").textContent=kView.toUpperCase();
|
|
3092
|
-
var af=_kAutoFitVals();
|
|
3093
|
-
setKSpaceTarget(trx, tr, af.scl, af.panX, af.panY, false);
|
|
3342
|
+
applyKProjection(K_VIEWS[(K_VIEWS.indexOf(kView)+1)%K_VIEWS.length],true);
|
|
3094
3343
|
};
|
|
3095
3344
|
|
|
3096
3345
|
function resizeKc(){
|
|
@@ -3224,7 +3473,7 @@ window.addEventListener("mousemove",function(e){
|
|
|
3224
3473
|
if(!kDragging||!kDragPrev||!kOpen||panelMode!=="kspace")return;
|
|
3225
3474
|
var dx=e.clientX-kDragPrev.x, dy=e.clientY-kDragPrev.y;
|
|
3226
3475
|
kDragPrev={x:e.clientX,y:e.clientY};
|
|
3227
|
-
|
|
3476
|
+
kLeavePreset();
|
|
3228
3477
|
if(kDragBtn===0){
|
|
3229
3478
|
// left drag = instant rotate (no lerp — feels responsive)
|
|
3230
3479
|
kRotY+=dx*0.008; kRotX-=dy*0.008;
|
|
@@ -3267,7 +3516,7 @@ kCanvas.addEventListener("touchstart",function(e){
|
|
|
3267
3516
|
},{passive:false});
|
|
3268
3517
|
kCanvas.addEventListener("touchmove",function(e){
|
|
3269
3518
|
if(!_kTouchActive||!_kTouchPrev||!kOpen||panelMode!=="kspace")return;
|
|
3270
|
-
|
|
3519
|
+
kLeavePreset();
|
|
3271
3520
|
if(e.touches.length===1&&_kTouchBtn===0){
|
|
3272
3521
|
// 1‑finger rotate
|
|
3273
3522
|
var dx=e.touches[0].clientX-_kTouchPrev.x;
|
|
@@ -3382,8 +3631,8 @@ window.addEventListener("resize",function(){if(kOpen){resizeKc();drawKs();}});
|
|
|
3382
3631
|
applyLayoutMode()/setPanelMode() read it back on every open. */
|
|
3383
3632
|
function persistPanelSize(){
|
|
3384
3633
|
try{
|
|
3385
|
-
if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')
|
|
3386
|
-
else
|
|
3634
|
+
if(typeof layoutMode!=='undefined'&&layoutMode==='vertical')SeqEyesPrefs.set('seqeyes.panelHeight',kResizeH);
|
|
3635
|
+
else SeqEyesPrefs.set('seqeyes.panelWidth',kResizeW);
|
|
3387
3636
|
}catch(_){/* private mode */}
|
|
3388
3637
|
}
|
|
3389
3638
|
|
|
@@ -4724,18 +4973,12 @@ var SeqEyesPanel = (function () {
|
|
|
4724
4973
|
var host = null;
|
|
4725
4974
|
var wired = false;
|
|
4726
4975
|
|
|
4727
|
-
/* ── Persistence
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
return isFinite(value) ? value : fallback;
|
|
4734
|
-
}
|
|
4735
|
-
function getBool(key, fallback) {
|
|
4736
|
-
var raw = get(key);
|
|
4737
|
-
return raw === null ? fallback : raw === '1';
|
|
4738
|
-
}
|
|
4976
|
+
/* ── Persistence ──────────────────────────────────────────────────────
|
|
4977
|
+
Delegated so the master switch in the settings popover governs the
|
|
4978
|
+
spectrogram parameters too; this module used to own a fourth copy of
|
|
4979
|
+
the same get/set pair. */
|
|
4980
|
+
var get = SeqEyesPrefs.get, set = SeqEyesPrefs.set;
|
|
4981
|
+
var getNum = SeqEyesPrefs.getNum, getBool = SeqEyesPrefs.getBool;
|
|
4739
4982
|
|
|
4740
4983
|
/* ── Parameters and view state ────────────────────────────────────── */
|
|
4741
4984
|
var params = {
|
|
@@ -6399,6 +6642,196 @@ window.SeqEyesDev.setSplitRatio = function (r) { SeqEyesPanel.setSplitRatio(r);
|
|
|
6399
6642
|
window.SeqEyesDev.getSplitRatio = function () { return SeqEyesPanel.getSplitRatio(); };
|
|
6400
6643
|
|
|
6401
6644
|
|
|
6645
|
+
/* ══ prefs-ui.js ══ */
|
|
6646
|
+
/* ═══════════════════════════════════════════════════════════════════════
|
|
6647
|
+
Settings popover — the consent surface for SeqEyesPrefs
|
|
6648
|
+
|
|
6649
|
+
One ⚙ chip in the toolbar overflow, opening a small dialog that says what
|
|
6650
|
+
the viewer keeps, lets the user turn that off, and throws away what is
|
|
6651
|
+
already stored. Persistence is useless to someone who cannot see it or
|
|
6652
|
+
revoke it, and the ASC cache in particular puts the contents of a scanner
|
|
6653
|
+
hardware file into browser storage — that deserves a visible switch rather
|
|
6654
|
+
than a line in the README.
|
|
6655
|
+
|
|
6656
|
+
It borrows the `.lblc` popover styles from the label marker controls
|
|
6657
|
+
instead of growing a second popup look.
|
|
6658
|
+
═══════════════════════════════════════════════════════════════════════ */
|
|
6659
|
+
|
|
6660
|
+
var SeqEyesPrefsUi = (function () {
|
|
6661
|
+
var popover = null, anchor = null, rememberBox = null, summaryEl = null;
|
|
6662
|
+
|
|
6663
|
+
function element(tag, className, text) {
|
|
6664
|
+
var el = document.createElement(tag);
|
|
6665
|
+
if (className) el.className = className;
|
|
6666
|
+
if (text !== undefined && text !== null) el.textContent = text;
|
|
6667
|
+
return el;
|
|
6668
|
+
}
|
|
6669
|
+
|
|
6670
|
+
function inVsCode() { return typeof vscApi !== 'undefined' && !!vscApi; }
|
|
6671
|
+
|
|
6672
|
+
function whereText() {
|
|
6673
|
+
return inVsCode()
|
|
6674
|
+
? 'this copy of VS Code'
|
|
6675
|
+
: 'this browser';
|
|
6676
|
+
}
|
|
6677
|
+
|
|
6678
|
+
/* Plain counts rather than a key dump: enough to tell whether "Forget" will
|
|
6679
|
+
do anything, without turning the dialog into a storage inspector. */
|
|
6680
|
+
function summarise() {
|
|
6681
|
+
if (!SeqEyesPrefs.enabled()) return 'Nothing is being stored.';
|
|
6682
|
+
var stored = SeqEyesPrefs.storedKeys();
|
|
6683
|
+
if (!stored.length) return 'Nothing stored yet.';
|
|
6684
|
+
var asc = SeqEyesPrefs.getAsc();
|
|
6685
|
+
var line = stored.length + ' setting' + (stored.length === 1 ? '' : 's') + ' stored';
|
|
6686
|
+
return asc ? line + ', including the ASC profile ' + asc.name + '.' : line + '.';
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
function refresh() {
|
|
6690
|
+
if (rememberBox) rememberBox.checked = SeqEyesPrefs.enabled();
|
|
6691
|
+
if (summaryEl) summaryEl.textContent = summarise();
|
|
6692
|
+
}
|
|
6693
|
+
|
|
6694
|
+
function render() {
|
|
6695
|
+
popover.textContent = '';
|
|
6696
|
+
|
|
6697
|
+
var head = element('div', 'lblc-head');
|
|
6698
|
+
head.appendChild(element('span', null, 'SeqEyes settings'));
|
|
6699
|
+
var close = element('button', 'lblc-close', '✕');
|
|
6700
|
+
close.type = 'button';
|
|
6701
|
+
close.setAttribute('aria-label', 'Close');
|
|
6702
|
+
close.onclick = function () { closeControls(true); };
|
|
6703
|
+
head.appendChild(close);
|
|
6704
|
+
|
|
6705
|
+
var list = element('div', 'lblc-list');
|
|
6706
|
+
|
|
6707
|
+
var row = element('div', 'lblc-row');
|
|
6708
|
+
rememberBox = document.createElement('input');
|
|
6709
|
+
rememberBox.type = 'checkbox';
|
|
6710
|
+
rememberBox.id = 'prefsRemember';
|
|
6711
|
+
rememberBox.checked = SeqEyesPrefs.enabled();
|
|
6712
|
+
var label = element('label', 'prefs-label', 'Remember my settings on ' + whereText());
|
|
6713
|
+
label.setAttribute('for', 'prefsRemember');
|
|
6714
|
+
rememberBox.onchange = function () {
|
|
6715
|
+
SeqEyesPrefs.setEnabled(this.checked);
|
|
6716
|
+
refresh();
|
|
6717
|
+
};
|
|
6718
|
+
row.appendChild(rememberBox);
|
|
6719
|
+
row.appendChild(label);
|
|
6720
|
+
list.appendChild(row);
|
|
6721
|
+
|
|
6722
|
+
var note = element('div', 'prefs-note');
|
|
6723
|
+
note.appendChild(element('div', null,
|
|
6724
|
+
'Theme, time and gradient units, the Blocks toggle, k-space and '
|
|
6725
|
+
+ 'spectrogram options, and panel sizes are kept between sessions.'));
|
|
6726
|
+
note.appendChild(element('div', null, inVsCode()
|
|
6727
|
+
/* Different sentences because the two hosts really do keep different
|
|
6728
|
+
things: a path the extension re-reads, versus the file's text. */
|
|
6729
|
+
? 'A loaded ASC profile is remembered by its file path and re-read when '
|
|
6730
|
+
+ 'you open a sequence. If the file moves, it is quietly forgotten.'
|
|
6731
|
+
: 'A loaded ASC profile is stored as text in this browser so it can be '
|
|
6732
|
+
+ 'restored without picking the file again.'));
|
|
6733
|
+
list.appendChild(note);
|
|
6734
|
+
|
|
6735
|
+
summaryEl = element('div', 'prefs-note prefs-summary', summarise());
|
|
6736
|
+
list.appendChild(summaryEl);
|
|
6737
|
+
|
|
6738
|
+
var foot = element('div', 'lblc-foot');
|
|
6739
|
+
var forget = element('button', null, 'Forget stored settings');
|
|
6740
|
+
forget.type = 'button';
|
|
6741
|
+
forget.title = 'Delete everything SeqEyes has stored. The current view is left as it is.';
|
|
6742
|
+
forget.onclick = function () {
|
|
6743
|
+
SeqEyesPrefs.forget();
|
|
6744
|
+
refresh();
|
|
6745
|
+
};
|
|
6746
|
+
foot.appendChild(forget);
|
|
6747
|
+
|
|
6748
|
+
popover.appendChild(head);
|
|
6749
|
+
popover.appendChild(list);
|
|
6750
|
+
popover.appendChild(foot);
|
|
6751
|
+
}
|
|
6752
|
+
|
|
6753
|
+
/* Same placement rules as the label popover: below the chip when it fits,
|
|
6754
|
+
flipped above when it does not, and a bottom sheet on narrow screens. */
|
|
6755
|
+
function positionPopover() {
|
|
6756
|
+
if (!popover) return;
|
|
6757
|
+
var compact = !!(window.matchMedia && window.matchMedia('(max-width: 768px), (pointer: coarse)').matches);
|
|
6758
|
+
popover.classList.toggle('sheet', compact);
|
|
6759
|
+
if (compact) { popover.style.left = ''; popover.style.top = ''; return; }
|
|
6760
|
+
var rect = anchor && anchor.isConnected ? anchor.getBoundingClientRect() : { left: 8, top: 8, bottom: 8 };
|
|
6761
|
+
var width = popover.offsetWidth, height = popover.offsetHeight;
|
|
6762
|
+
var vw = window.innerWidth, vh = window.innerHeight;
|
|
6763
|
+
var top = rect.bottom + 4;
|
|
6764
|
+
if (top + height > vh - 8) top = Math.max(8, rect.top - height - 4);
|
|
6765
|
+
if (top + height > vh - 8) top = Math.max(8, vh - height - 8);
|
|
6766
|
+
popover.style.left = Math.max(8, Math.min(rect.left, vw - width - 8)) + 'px';
|
|
6767
|
+
popover.style.top = top + 'px';
|
|
6768
|
+
}
|
|
6769
|
+
|
|
6770
|
+
function onKeyDown(event) {
|
|
6771
|
+
if (event.key !== 'Escape') return;
|
|
6772
|
+
event.preventDefault(); event.stopPropagation();
|
|
6773
|
+
closeControls(true);
|
|
6774
|
+
}
|
|
6775
|
+
|
|
6776
|
+
function onPointerDown(event) {
|
|
6777
|
+
var target = event.target;
|
|
6778
|
+
if (popover.contains(target)) return;
|
|
6779
|
+
// The chip toggles on its own click; closing here first would reopen it.
|
|
6780
|
+
if (target && target.closest && target.closest('#prefsBtn')) return;
|
|
6781
|
+
closeControls(false);
|
|
6782
|
+
}
|
|
6783
|
+
|
|
6784
|
+
function openControls(chip) {
|
|
6785
|
+
closeControls(false);
|
|
6786
|
+
popover = element('div', 'lblc prefs-pop');
|
|
6787
|
+
popover.id = 'prefsControls';
|
|
6788
|
+
popover.setAttribute('role', 'dialog');
|
|
6789
|
+
popover.setAttribute('aria-label', 'SeqEyes settings');
|
|
6790
|
+
anchor = chip;
|
|
6791
|
+
render();
|
|
6792
|
+
document.body.appendChild(popover);
|
|
6793
|
+
if (anchor) anchor.setAttribute('aria-expanded', 'true');
|
|
6794
|
+
positionPopover();
|
|
6795
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
6796
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
6797
|
+
window.addEventListener('resize', positionPopover);
|
|
6798
|
+
if (rememberBox) rememberBox.focus();
|
|
6799
|
+
}
|
|
6800
|
+
|
|
6801
|
+
function closeControls(restoreFocus) {
|
|
6802
|
+
if (!popover) return;
|
|
6803
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
6804
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
6805
|
+
window.removeEventListener('resize', positionPopover);
|
|
6806
|
+
if (popover.parentNode) popover.parentNode.removeChild(popover);
|
|
6807
|
+
var chip = anchor;
|
|
6808
|
+
popover = null; anchor = null; rememberBox = null; summaryEl = null;
|
|
6809
|
+
if (chip) {
|
|
6810
|
+
chip.setAttribute('aria-expanded', 'false');
|
|
6811
|
+
if (restoreFocus && chip.isConnected) chip.focus();
|
|
6812
|
+
}
|
|
6813
|
+
}
|
|
6814
|
+
|
|
6815
|
+
/* Idempotent, because web/index.html installs its own handlers over the
|
|
6816
|
+
bundle's and may call this again. */
|
|
6817
|
+
var wired = false;
|
|
6818
|
+
function install() {
|
|
6819
|
+
if (wired) return;
|
|
6820
|
+
var chip = document.getElementById('prefsBtn');
|
|
6821
|
+
if (!chip) return;
|
|
6822
|
+
wired = true;
|
|
6823
|
+
chip.onclick = function () { if (popover) closeControls(false); else openControls(chip); };
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
return {
|
|
6827
|
+
install: install,
|
|
6828
|
+
open: openControls,
|
|
6829
|
+
close: closeControls,
|
|
6830
|
+
isOpen: function () { return !!popover; }
|
|
6831
|
+
};
|
|
6832
|
+
})();
|
|
6833
|
+
|
|
6834
|
+
|
|
6402
6835
|
/* ══ interaction.js ══ */
|
|
6403
6836
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
6404
6837
|
Mouse interaction
|
|
@@ -6672,7 +7105,9 @@ document.getElementById('zi').onclick=function(){if(zoomAtCenter(1.5))scheduleVi
|
|
|
6672
7105
|
document.getElementById('zo').onclick=function(){if(zoomAtCenter(1/1.5))scheduleViewerDraw(true);};
|
|
6673
7106
|
document.getElementById('zf').onclick=function(){fit();drawMinimap();};
|
|
6674
7107
|
document.getElementById('zr').onclick=function(){fit();drawMinimap();};
|
|
6675
|
-
document.getElementById('bbc')
|
|
7108
|
+
var bbCheck=document.getElementById('bbc');
|
|
7109
|
+
bbCheck.checked=showBB; // state.js read the remembered value; match the box to it
|
|
7110
|
+
bbCheck.onchange=function(){showBB=this.checked;SeqEyesPrefs.setBool(SeqEyesPrefs.KEYS.showBlocks,showBB);draw();};
|
|
6676
7111
|
|
|
6677
7112
|
/* ── Mobile hamburger menu ─────────────────────────────────────────── */
|
|
6678
7113
|
var menuBtn=document.getElementById('menuBtn');
|
|
@@ -6773,6 +7208,7 @@ rs();
|
|
|
6773
7208
|
the host adapter to exist. web/index.html installs its own adapter over
|
|
6774
7209
|
this one — install() swaps the host without re-binding the DOM. */
|
|
6775
7210
|
SeqEyesPanel.install(window.SeqEyesPanelHost);
|
|
7211
|
+
SeqEyesPrefsUi.install();
|
|
6776
7212
|
/* Deferred by one macrotask so web/index.html, whose inline IIFE runs after
|
|
6777
7213
|
this bundle, has installed its own adapter before the persisted mode is
|
|
6778
7214
|
restored against it. */
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|