seqeyes-python 0.3.5__tar.gz → 0.3.7__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.5 → seqeyes_python-0.3.7}/PKG-INFO +1 -1
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/pyproject.toml +1 -1
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/resources/pulseq-bundle.js +47 -22
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/resources/webview-bundle.js +51 -41
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/.gitignore +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/README.md +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/examples/demo.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.5 → seqeyes_python-0.3.7}/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.7
|
|
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.7"
|
|
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"}
|
|
@@ -23,31 +23,24 @@ var Pulseq = (() => {
|
|
|
23
23
|
// web/pulseq-browser.ts
|
|
24
24
|
var pulseq_browser_exports = {};
|
|
25
25
|
__export(pulseq_browser_exports, {
|
|
26
|
-
BAND_DETAIL_SAMPLES: () => BAND_DETAIL_SAMPLES,
|
|
27
|
-
EXACT_DETAIL_SAMPLES: () => EXACT_DETAIL_SAMPLES,
|
|
28
26
|
INTERACTIVE_COMPUTE_LIMITS: () => INTERACTIVE_COMPUTE_LIMITS,
|
|
29
|
-
MAX_DETAIL_PTS: () => MAX_DETAIL_PTS,
|
|
30
|
-
MAX_DISPLAY_PTS: () => MAX_DISPLAY_PTS,
|
|
31
|
-
MAX_ENVELOPE_COLUMNS: () => MAX_ENVELOPE_COLUMNS,
|
|
32
27
|
MAX_RF_RESPONSE_BANDS: () => MAX_RF_RESPONSE_BANDS,
|
|
33
28
|
MAX_RF_RESPONSE_FFT_POINTS: () => MAX_RF_RESPONSE_FFT_POINTS,
|
|
34
29
|
MAX_RF_RESPONSE_SAMPLES: () => MAX_RF_RESPONSE_SAMPLES,
|
|
35
30
|
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
36
31
|
analyzeRfResponse: () => analyzeRfResponse,
|
|
37
32
|
audioBudgetRefusal: () => audioBudgetRefusal,
|
|
33
|
+
buildWaveformDetailReply: () => buildWaveformDetailReply,
|
|
38
34
|
calculateKspace: () => calculateKspace,
|
|
39
35
|
calculateM1: () => calculateM1,
|
|
40
36
|
calculateM1Coarse: () => calculateM1Coarse,
|
|
41
37
|
calculatePns: () => calculatePns,
|
|
42
38
|
calculatePnsCoarse: () => calculatePnsCoarse,
|
|
43
39
|
computeGradSpectrumParity: () => computeGradSpectrumParity,
|
|
44
|
-
computeGradientEnvelope: () => computeGradientEnvelope,
|
|
45
40
|
computeGradientSpectrogram: () => computeGradientSpectrogram,
|
|
46
41
|
computeGradientSpectrumAverage: () => computeGradientSpectrumAverage,
|
|
47
42
|
computeGradientSpectrumSlice: () => computeGradientSpectrumSlice,
|
|
48
43
|
countBandsOutsideRange: () => countBandsOutsideRange,
|
|
49
|
-
countExactDetailSamples: () => countExactDetailSamples,
|
|
50
|
-
countGradientSamples: () => countGradientSamples,
|
|
51
44
|
createSequenceDecodeContext: () => createSequenceDecodeContext,
|
|
52
45
|
decodeAllBlocks: () => decodeAllBlocks,
|
|
53
46
|
decodeBlockRange: () => decodeBlockRange,
|
|
@@ -72,8 +65,6 @@ var Pulseq = (() => {
|
|
|
72
65
|
hasPulseqBinaryMagic: () => hasPulseqBinaryMagic,
|
|
73
66
|
isEmptyAscProfile: () => isEmptyAscProfile,
|
|
74
67
|
kspaceExceedsInteractiveBudget: () => kspaceExceedsInteractiveBudget,
|
|
75
|
-
packGradientEnvelope: () => packGradientEnvelope,
|
|
76
|
-
packSequenceBlockRange: () => packSequenceBlockRange,
|
|
77
68
|
packSequenceBlocks: () => packSequenceBlocks,
|
|
78
69
|
parseAcousticResonancesAsc: () => parseAcousticResonancesAsc,
|
|
79
70
|
parseAscProfile: () => parseAscProfile,
|
|
@@ -84,7 +75,6 @@ var Pulseq = (() => {
|
|
|
84
75
|
parseSequenceText: () => parseSequenceText,
|
|
85
76
|
physicalGradientValueAt: () => physicalGradientValueAt,
|
|
86
77
|
resamplePhysicalGradients: () => resamplePhysicalGradients,
|
|
87
|
-
resolveDetailBlockRange: () => resolveDetailBlockRange,
|
|
88
78
|
resolveSpectrogramParams: () => resolveSpectrogramParams,
|
|
89
79
|
rotateGradient: () => rotateGradient,
|
|
90
80
|
safePnsModel: () => safePnsModel,
|
|
@@ -97,7 +87,7 @@ var Pulseq = (() => {
|
|
|
97
87
|
});
|
|
98
88
|
|
|
99
89
|
// package.json
|
|
100
|
-
var version = "0.3.
|
|
90
|
+
var version = "0.3.7";
|
|
101
91
|
|
|
102
92
|
// src/pulseq/decompressor.ts
|
|
103
93
|
function decompressShape(compressed, numSamples) {
|
|
@@ -2862,16 +2852,6 @@ var Pulseq = (() => {
|
|
|
2862
2852
|
}
|
|
2863
2853
|
return { startSec, endSec, columns: width, channels };
|
|
2864
2854
|
}
|
|
2865
|
-
function countGradientSamples(blocks) {
|
|
2866
|
-
let total = 0;
|
|
2867
|
-
for (const block of blocks) {
|
|
2868
|
-
for (const key of GRADIENT_CHANNELS) {
|
|
2869
|
-
const grad = block[key];
|
|
2870
|
-
if (grad && grad.type !== "none") total += grad.timePoints.length;
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
return total;
|
|
2874
|
-
}
|
|
2875
2855
|
function packGradientEnvelope(envelope) {
|
|
2876
2856
|
const { columns } = envelope;
|
|
2877
2857
|
const out = new Float32Array(columns * 6);
|
|
@@ -2886,6 +2866,51 @@ var Pulseq = (() => {
|
|
|
2886
2866
|
return { startSec: envelope.startSec, endSec: envelope.endSec, columns, values: out.buffer };
|
|
2887
2867
|
}
|
|
2888
2868
|
|
|
2869
|
+
// src/editor/waveformDetailReply.ts
|
|
2870
|
+
var WINDOW_DETAIL_BLOCK_LIMIT = 2e4;
|
|
2871
|
+
function buildWaveformDetailReply(seq, context, request, generation = 0, lookup, blockLimit = WINDOW_DETAIL_BLOCK_LIMIT) {
|
|
2872
|
+
const startSec = Number(request.startSec);
|
|
2873
|
+
const endSec = Number(request.endSec);
|
|
2874
|
+
const { start, end } = resolveDetailBlockRange(
|
|
2875
|
+
context.blockStartTimes,
|
|
2876
|
+
seq.blocks.length,
|
|
2877
|
+
startSec,
|
|
2878
|
+
endSec
|
|
2879
|
+
);
|
|
2880
|
+
if (!(endSec > startSec)) {
|
|
2881
|
+
return { kind: "error", message: "This waveform detail window is empty." };
|
|
2882
|
+
}
|
|
2883
|
+
const columns = Math.max(1, Math.min(
|
|
2884
|
+
MAX_ENVELOPE_COLUMNS,
|
|
2885
|
+
Math.floor(Number(request.columns) || 0) || 1
|
|
2886
|
+
));
|
|
2887
|
+
const cacheKey = `${generation}:${start}:${end}:${startSec}:${endSec}`;
|
|
2888
|
+
const cached = lookup?.(cacheKey);
|
|
2889
|
+
if (cached && end - start <= blockLimit) {
|
|
2890
|
+
return { kind: "samples", startBlock: start, endBlock: end, startSec, endSec, packed: cached, cacheKey };
|
|
2891
|
+
}
|
|
2892
|
+
const decoded = decodeBlockRange(seq, start, end, context);
|
|
2893
|
+
const windowSamples = countExactDetailSamples(decoded, { startSec, endSec });
|
|
2894
|
+
if (windowSamples > EXACT_DETAIL_SAMPLES) {
|
|
2895
|
+
if (windowSamples > BAND_DETAIL_SAMPLES) return { kind: "unavailable", startSec, endSec };
|
|
2896
|
+
const band = packGradientEnvelope(computeGradientEnvelope(decoded, startSec, endSec, columns));
|
|
2897
|
+
return { kind: "band", startSec: band.startSec, endSec: band.endSec, columns: band.columns, values: band.values };
|
|
2898
|
+
}
|
|
2899
|
+
if (end - start > blockLimit) {
|
|
2900
|
+
return { kind: "error", message: "This waveform detail window is too large. Zoom in further." };
|
|
2901
|
+
}
|
|
2902
|
+
const packed = packSequenceBlockRange(
|
|
2903
|
+
seq,
|
|
2904
|
+
start,
|
|
2905
|
+
end,
|
|
2906
|
+
context,
|
|
2907
|
+
MAX_DETAIL_PTS,
|
|
2908
|
+
void 0,
|
|
2909
|
+
{ startSec, endSec }
|
|
2910
|
+
);
|
|
2911
|
+
return { kind: "samples", startBlock: start, endBlock: end, startSec, endSec, packed, cacheKey };
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2889
2914
|
// src/pulseq/physicalGradients.ts
|
|
2890
2915
|
var GRADIENT_ENDPOINT_TOLERANCE_SEC = 1e-12;
|
|
2891
2916
|
function rotateGradient(block, gx, gy, gz) {
|
|
@@ -2277,7 +2277,17 @@ function drawTriggerBlocks(start,end,vi,ch,colors,vs,ve){
|
|
|
2277
2277
|
/* ══ kspace.js ══ */
|
|
2278
2278
|
var kOpen=false, kView="3d";
|
|
2279
2279
|
var kSpaceTrajectoryDrawCount=0,kSpaceOverlayDrawCount=0;
|
|
2280
|
-
|
|
2280
|
+
/**
|
|
2281
|
+
* Pan is a screen offset in CSS pixels, applied after rotation; the rotation
|
|
2282
|
+
* pivot is k = 0 and never moves.
|
|
2283
|
+
*
|
|
2284
|
+
* These used to be one world-space point that was both the pivot and whatever
|
|
2285
|
+
* sat at canvas centre, so panning relocated the pivot and a later rotation
|
|
2286
|
+
* swung the cloud around empty space beside it. Separating them costs the
|
|
2287
|
+
* ability to orbit an off-centre feature, which is recorded as a deliberate
|
|
2288
|
+
* trade in plans/todo/KSPACE_ROTATION_CENTER.md.
|
|
2289
|
+
*/
|
|
2290
|
+
var kPanX=0, kPanY=0, kScl=1;
|
|
2281
2291
|
var kAutoFit=true;
|
|
2282
2292
|
var kRotX=-0.5, kRotY=0.7; // default 3D perspective
|
|
2283
2293
|
var kDragging=false, kDragPrev=null, kDragBtn=0;
|
|
@@ -2292,7 +2302,7 @@ var kCanvas=document.getElementById("kc"), kCtx=kCanvas.getContext("2d");
|
|
|
2292
2302
|
var kDotSize=2, kUnit="cyc"; // cyc=1/m, rad=rad/m
|
|
2293
2303
|
|
|
2294
2304
|
// ── Smooth animation targets ──────────────────────────────────────────
|
|
2295
|
-
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl,
|
|
2305
|
+
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl, _tPanX=kPanX, _tPanY=kPanY;
|
|
2296
2306
|
var _kAnimId=null;
|
|
2297
2307
|
var _kEasing=0.12; // higher = snappier, lower = smoother (0.06–0.20)
|
|
2298
2308
|
|
|
@@ -2304,15 +2314,14 @@ function startKSpaceAnim() {
|
|
|
2304
2314
|
if (Math.abs(_tRotX - kRotX) > eps) { kRotX += (_tRotX - kRotX) * _kEasing; changed = true; }
|
|
2305
2315
|
if (Math.abs(_tRotY - kRotY) > eps) { kRotY += (_tRotY - kRotY) * _kEasing; changed = true; }
|
|
2306
2316
|
if (Math.abs(_tScl - kScl) > 0.001){ kScl += (_tScl - kScl) * _kEasing; changed = true; }
|
|
2307
|
-
if (Math.abs(
|
|
2308
|
-
if (Math.abs(
|
|
2309
|
-
if (Math.abs(_tCz - kCz) > 0.001){ kCz += (_tCz - kCz) * _kEasing; changed = true; }
|
|
2317
|
+
if (Math.abs(_tPanX - kPanX) > 0.01) { kPanX += (_tPanX - kPanX) * _kEasing; changed = true; }
|
|
2318
|
+
if (Math.abs(_tPanY - kPanY) > 0.01) { kPanY += (_tPanY - kPanY) * _kEasing; changed = true; }
|
|
2310
2319
|
if (changed) {
|
|
2311
2320
|
drawKsFast();
|
|
2312
2321
|
_kAnimId = requestAnimationFrame(tick);
|
|
2313
2322
|
} else {
|
|
2314
2323
|
// Snap to exact targets
|
|
2315
|
-
kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl;
|
|
2324
|
+
kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl; kPanX=_tPanX; kPanY=_tPanY;
|
|
2316
2325
|
_kAnimId = null;
|
|
2317
2326
|
drawKsFast(); // no longer moving: redraw the complete cloud
|
|
2318
2327
|
}
|
|
@@ -2320,12 +2329,12 @@ function startKSpaceAnim() {
|
|
|
2320
2329
|
_kAnimId = requestAnimationFrame(tick);
|
|
2321
2330
|
}
|
|
2322
2331
|
|
|
2323
|
-
function setKSpaceTarget(rx, ry, s,
|
|
2332
|
+
function setKSpaceTarget(rx, ry, s, panX, panY, instant) {
|
|
2324
2333
|
if (instant) {
|
|
2325
|
-
kRotX=_tRotX=rx; kRotY=_tRotY=ry; kScl=_tScl=s;
|
|
2334
|
+
kRotX=_tRotX=rx; kRotY=_tRotY=ry; kScl=_tScl=s; kPanX=_tPanX=panX; kPanY=_tPanY=panY;
|
|
2326
2335
|
drawKs();
|
|
2327
2336
|
} else {
|
|
2328
|
-
_tRotX=rx; _tRotY=ry; _tScl=s;
|
|
2337
|
+
_tRotX=rx; _tRotY=ry; _tScl=s; _tPanX=panX; _tPanY=panY;
|
|
2329
2338
|
startKSpaceAnim();
|
|
2330
2339
|
}
|
|
2331
2340
|
}
|
|
@@ -2379,7 +2388,7 @@ if(systemThemeQuery){
|
|
|
2379
2388
|
═══════════════════════════════════════════════════════════════════════ */
|
|
2380
2389
|
var gl=null, glProgram=null, glBuf=null, glN=0;
|
|
2381
2390
|
var glAttribPos=-1, glAttribTime=-1;
|
|
2382
|
-
var glU_cy=-1,glU_sy=-1,glU_cx=-1,glU_sx=-1,
|
|
2391
|
+
var glU_cy=-1,glU_sy=-1,glU_cx=-1,glU_sx=-1,glU_pan=-1,glU_scale=-1;
|
|
2383
2392
|
var glU_halfRes=-1,glU_tMin=-1,glU_tMax=-1,glU_dot=-1,glU_color=-1;
|
|
2384
2393
|
|
|
2385
2394
|
// ── Cached bounds (computed once when data is uploaded) ──────────────
|
|
@@ -2397,16 +2406,16 @@ function initWebGL(){
|
|
|
2397
2406
|
gl.shaderSource(vs,'\
|
|
2398
2407
|
attribute vec3 aPos; attribute float aTime;\
|
|
2399
2408
|
uniform float uCy,uSy,uCx,uSx,uScale;\
|
|
2400
|
-
uniform
|
|
2409
|
+
uniform vec2 uPan; uniform vec2 uHalfRes;\
|
|
2401
2410
|
uniform float uTMin,uTMax,uDot;\
|
|
2402
2411
|
varying float vVis;\
|
|
2403
2412
|
void main(){\
|
|
2404
|
-
float dx=aPos.x
|
|
2413
|
+
float dx=aPos.x,dy=aPos.y,dz=aPos.z;\
|
|
2405
2414
|
float rx=dx*uCy-dz*uSy;\
|
|
2406
2415
|
float rz=dx*uSy+dz*uCy;\
|
|
2407
2416
|
float ry=dy*uCx-rz*uSx;\
|
|
2408
|
-
float sx=rx*uScale/uHalfRes.x;\
|
|
2409
|
-
float sy=ry*uScale/uHalfRes.y;\
|
|
2417
|
+
float sx=(rx*uScale+uPan.x)/uHalfRes.x;\
|
|
2418
|
+
float sy=(ry*uScale-uPan.y)/uHalfRes.y;\
|
|
2410
2419
|
vVis=(aTime>=uTMin&&aTime<=uTMax)?1.0:-1.0;\
|
|
2411
2420
|
gl_Position=vec4(sx,sy,0.0,1.0);\
|
|
2412
2421
|
gl_PointSize=vVis>0.0?uDot:0.0;\
|
|
@@ -2431,7 +2440,7 @@ function initWebGL(){
|
|
|
2431
2440
|
glAttribTime=gl.getAttribLocation(glProgram,"aTime");
|
|
2432
2441
|
glU_cy=gl.getUniformLocation(glProgram,"uCy");glU_sy=gl.getUniformLocation(glProgram,"uSy");
|
|
2433
2442
|
glU_cx=gl.getUniformLocation(glProgram,"uCx");glU_sx=gl.getUniformLocation(glProgram,"uSx");
|
|
2434
|
-
|
|
2443
|
+
glU_pan=gl.getUniformLocation(glProgram,"uPan");glU_scale=gl.getUniformLocation(glProgram,"uScale");
|
|
2435
2444
|
glU_halfRes=gl.getUniformLocation(glProgram,"uHalfRes");glU_dot=gl.getUniformLocation(glProgram,"uDot");
|
|
2436
2445
|
glU_tMin=gl.getUniformLocation(glProgram,"uTMin");glU_tMax=gl.getUniformLocation(glProgram,"uTMax");
|
|
2437
2446
|
glU_color=gl.getUniformLocation(glProgram,"uColor");
|
|
@@ -2536,7 +2545,7 @@ document.getElementById("krst").onclick=function(){
|
|
|
2536
2545
|
kView="3d";
|
|
2537
2546
|
document.getElementById("kax").textContent="3D";
|
|
2538
2547
|
var af=_kAutoFitVals();
|
|
2539
|
-
setKSpaceTarget(-0.5, 0.7, af.scl, af.
|
|
2548
|
+
setKSpaceTarget(-0.5, 0.7, af.scl, af.panX, af.panY, false);
|
|
2540
2549
|
};
|
|
2541
2550
|
// Camera presets: smoothly rotate to look straight down an axis
|
|
2542
2551
|
document.getElementById("kax").onclick=function(){
|
|
@@ -2549,7 +2558,7 @@ document.getElementById("kax").onclick=function(){
|
|
|
2549
2558
|
else{trx=-0.5; tr=0.7;} // 3d — default perspective
|
|
2550
2559
|
document.getElementById("kax").textContent=kView.toUpperCase();
|
|
2551
2560
|
var af=_kAutoFitVals();
|
|
2552
|
-
setKSpaceTarget(trx, tr, af.scl, af.
|
|
2561
|
+
setKSpaceTarget(trx, tr, af.scl, af.panX, af.panY, false);
|
|
2553
2562
|
};
|
|
2554
2563
|
|
|
2555
2564
|
function resizeKc(){
|
|
@@ -2670,9 +2679,13 @@ kCanvas.addEventListener("wheel",function(e){e.preventDefault();
|
|
|
2670
2679
|
var r=kCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
|
|
2671
2680
|
var mx=(e.clientX-r.left)/dpr, my=(e.clientY-r.top)/dpr;
|
|
2672
2681
|
var zf=e.deltaY<0?1.25:0.8, W=kCanvas.width/dpr, H=kCanvas.height/dpr;
|
|
2673
|
-
var dz=(1-1/zf)*dpr/kScl;
|
|
2674
2682
|
kAutoFit=false;
|
|
2675
|
-
|
|
2683
|
+
// Keep whatever is under the cursor under the cursor. With pan in screen
|
|
2684
|
+
// space this is the same arithmetic at every rotation; the previous form
|
|
2685
|
+
// added a screen delta straight to world coordinates and only aimed
|
|
2686
|
+
// correctly in the unrotated view.
|
|
2687
|
+
setKSpaceTarget(kRotX, kRotY, kScl*zf,
|
|
2688
|
+
kPanX+(mx-W/2-kPanX)*(1-zf), kPanY+(my-H/2-kPanY)*(1-zf), false);
|
|
2676
2689
|
},{passive:false});
|
|
2677
2690
|
|
|
2678
2691
|
window.addEventListener("mousemove",function(e){
|
|
@@ -2685,13 +2698,10 @@ window.addEventListener("mousemove",function(e){
|
|
|
2685
2698
|
kRotY+=dx*0.008; kRotX-=dy*0.008;
|
|
2686
2699
|
_tRotY=kRotY; _tRotX=kRotX; // sync targets
|
|
2687
2700
|
}else{
|
|
2688
|
-
// right/middle drag = instant pan
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
if(Math.abs(cz)>0.01){kCy+=dy/cx;kCx+=(-dx-sz*sx*(dy/cx))/cz;}
|
|
2693
|
-
else{kCy+=dy/cx;kCz+=(dx-cz*sx*(dy/cx))/sz;}
|
|
2694
|
-
_tCy=kCy; _tCx=kCx; _tCz=kCz; // sync targets
|
|
2701
|
+
// right/middle drag = instant pan. Pan is a screen offset now, so it
|
|
2702
|
+
// needs no inverse rotation and cannot move the rotation pivot.
|
|
2703
|
+
kPanX+=dx; kPanY+=dy;
|
|
2704
|
+
_tPanX=kPanX; _tPanY=kPanY; // sync targets
|
|
2695
2705
|
kAutoFit=false;
|
|
2696
2706
|
}
|
|
2697
2707
|
scheduleKsDragDraw();
|
|
@@ -2746,12 +2756,8 @@ kCanvas.addEventListener("touchmove",function(e){
|
|
|
2746
2756
|
var mid=getTouchMid(e.touches);
|
|
2747
2757
|
var pdx=(mid.x-_kTouchPrev.x), pdy=(mid.y-_kTouchPrev.y);
|
|
2748
2758
|
_kTouchPrev={x:mid.x,y:mid.y};
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
pdx/=(dpr*kScl); pdy/=(dpr*kScl);
|
|
2752
|
-
if(Math.abs(cz)>0.01){kCy+=pdy/cx;kCx+=(-pdx-sz*sx*(pdy/cx))/cz;}
|
|
2753
|
-
else{kCy+=pdy/cx;kCz+=(pdx-cz*sx*(pdy/cx))/sz;}
|
|
2754
|
-
_tCy=kCy; _tCx=kCx; _tCz=kCz; kAutoFit=false;
|
|
2759
|
+
kPanX+=pdx; kPanY+=pdy;
|
|
2760
|
+
_tPanX=kPanX; _tPanY=kPanY; kAutoFit=false;
|
|
2755
2761
|
drawKsFast();
|
|
2756
2762
|
}
|
|
2757
2763
|
e.preventDefault();
|
|
@@ -2855,8 +2861,13 @@ function persistPanelSize(){
|
|
|
2855
2861
|
function kNice(range){var ms=[1,2,5,10,20,50,100,200,500];for(var i=0;i<ms.length;i++){var b=Math.pow(10,Math.floor(Math.log10(range)));if(ms[i]*b>=range/4)return ms[i]*b;}return 1;}
|
|
2856
2862
|
|
|
2857
2863
|
function _kAutoFitVals(){
|
|
2858
|
-
var
|
|
2859
|
-
|
|
2864
|
+
var W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1);
|
|
2865
|
+
// The pivot is k = 0, so what has to fit is the furthest point from the
|
|
2866
|
+
// origin, not the width of the bounding box.
|
|
2867
|
+
var reach=Math.max(Math.abs(_kBxmin),Math.abs(_kBxmax),Math.abs(_kBymin),
|
|
2868
|
+
Math.abs(_kBymax),Math.abs(_kBzmin),Math.abs(_kBzmax));
|
|
2869
|
+
if(!(reach>0)||!isFinite(reach))reach=(_kBrng||1)/2;
|
|
2870
|
+
return{panX:0,panY:0,scl:Math.min(W,H)/(reach*2*1.15)};
|
|
2860
2871
|
}
|
|
2861
2872
|
|
|
2862
2873
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
@@ -2927,7 +2938,7 @@ function drawKs_core(W,H,dpr){
|
|
|
2927
2938
|
if(kAutoFit){
|
|
2928
2939
|
kAutoFit=false;
|
|
2929
2940
|
var af=_kAutoFitVals();
|
|
2930
|
-
setKSpaceTarget(kRotX, kRotY, af.scl, af.
|
|
2941
|
+
setKSpaceTarget(kRotX, kRotY, af.scl, af.panX, af.panY, false);
|
|
2931
2942
|
}
|
|
2932
2943
|
|
|
2933
2944
|
// ── Time window ──
|
|
@@ -2944,7 +2955,7 @@ function drawKs_core(W,H,dpr){
|
|
|
2944
2955
|
|
|
2945
2956
|
gl.uniform1f(glU_cy,Math.cos(kRotY));gl.uniform1f(glU_sy,Math.sin(kRotY));
|
|
2946
2957
|
gl.uniform1f(glU_cx,Math.cos(kRotX));gl.uniform1f(glU_sx,Math.sin(kRotX));
|
|
2947
|
-
gl.
|
|
2958
|
+
gl.uniform2f(glU_pan,kPanX*dpr,kPanY*dpr);
|
|
2948
2959
|
gl.uniform1f(glU_scale,kScl);
|
|
2949
2960
|
gl.uniform2f(glU_halfRes,hW,hH);
|
|
2950
2961
|
gl.uniform1f(glU_tMin,vs);gl.uniform1f(glU_tMax,ve);
|
|
@@ -2990,11 +3001,10 @@ function drawKsOverlay(W,H,dpr,cs){
|
|
|
2990
3001
|
var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cxR=Math.cos(kRotX),sxR=Math.sin(kRotX);
|
|
2991
3002
|
var invDpr=1/dpr;
|
|
2992
3003
|
function proj(px,py,pz){
|
|
2993
|
-
var
|
|
2994
|
-
var
|
|
2995
|
-
var
|
|
2996
|
-
|
|
2997
|
-
return {x:W/2+rx*kScl*invDpr, y:H/2-ry*kScl*invDpr};
|
|
3004
|
+
var rx=px*cz-pz*sz;
|
|
3005
|
+
var rz2=px*sz+pz*cz;
|
|
3006
|
+
var ry=py*cxR-rz2*sxR;
|
|
3007
|
+
return {x:W/2+rx*kScl*invDpr+kPanX, y:H/2-ry*kScl*invDpr+kPanY};
|
|
2998
3008
|
}
|
|
2999
3009
|
var tick=kNice(rng3);
|
|
3000
3010
|
function drawAxis3D(fx,fy,fz,label,col){
|
|
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
|