seqeyes-python 0.3.4__tar.gz → 0.3.6__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.4 → seqeyes_python-0.3.6}/PKG-INFO +1 -1
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/pyproject.toml +1 -1
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/resources/pulseq-bundle.js +1 -1
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/resources/webview-bundle.js +147 -47
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/.gitignore +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/README.md +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/examples/demo.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.4 → seqeyes_python-0.3.6}/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.6
|
|
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.6"
|
|
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"}
|
|
@@ -2277,15 +2277,32 @@ 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;
|
|
2294
|
+
/**
|
|
2295
|
+
* Pending coalesced drag redraw. A drag updates the rotation on every
|
|
2296
|
+
* mousemove but only needs one draw per displayed frame; the frames in between
|
|
2297
|
+
* are overwritten before anyone sees them. The inertial path is already
|
|
2298
|
+
* requestAnimationFrame-driven, so this only brings the drag into line with it.
|
|
2299
|
+
*/
|
|
2300
|
+
var _kDragRaf=0;
|
|
2284
2301
|
var kCanvas=document.getElementById("kc"), kCtx=kCanvas.getContext("2d");
|
|
2285
2302
|
var kDotSize=2, kUnit="cyc"; // cyc=1/m, rad=rad/m
|
|
2286
2303
|
|
|
2287
2304
|
// ── Smooth animation targets ──────────────────────────────────────────
|
|
2288
|
-
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl,
|
|
2305
|
+
var _tRotX=kRotX, _tRotY=kRotY, _tScl=kScl, _tPanX=kPanX, _tPanY=kPanY;
|
|
2289
2306
|
var _kAnimId=null;
|
|
2290
2307
|
var _kEasing=0.12; // higher = snappier, lower = smoother (0.06–0.20)
|
|
2291
2308
|
|
|
@@ -2297,27 +2314,27 @@ function startKSpaceAnim() {
|
|
|
2297
2314
|
if (Math.abs(_tRotX - kRotX) > eps) { kRotX += (_tRotX - kRotX) * _kEasing; changed = true; }
|
|
2298
2315
|
if (Math.abs(_tRotY - kRotY) > eps) { kRotY += (_tRotY - kRotY) * _kEasing; changed = true; }
|
|
2299
2316
|
if (Math.abs(_tScl - kScl) > 0.001){ kScl += (_tScl - kScl) * _kEasing; changed = true; }
|
|
2300
|
-
if (Math.abs(
|
|
2301
|
-
if (Math.abs(
|
|
2302
|
-
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; }
|
|
2303
2319
|
if (changed) {
|
|
2304
2320
|
drawKsFast();
|
|
2305
2321
|
_kAnimId = requestAnimationFrame(tick);
|
|
2306
2322
|
} else {
|
|
2307
2323
|
// Snap to exact targets
|
|
2308
|
-
kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl;
|
|
2324
|
+
kRotX=_tRotX; kRotY=_tRotY; kScl=_tScl; kPanX=_tPanX; kPanY=_tPanY;
|
|
2309
2325
|
_kAnimId = null;
|
|
2326
|
+
drawKsFast(); // no longer moving: redraw the complete cloud
|
|
2310
2327
|
}
|
|
2311
2328
|
}
|
|
2312
2329
|
_kAnimId = requestAnimationFrame(tick);
|
|
2313
2330
|
}
|
|
2314
2331
|
|
|
2315
|
-
function setKSpaceTarget(rx, ry, s,
|
|
2332
|
+
function setKSpaceTarget(rx, ry, s, panX, panY, instant) {
|
|
2316
2333
|
if (instant) {
|
|
2317
|
-
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;
|
|
2318
2335
|
drawKs();
|
|
2319
2336
|
} else {
|
|
2320
|
-
_tRotX=rx; _tRotY=ry; _tScl=s;
|
|
2337
|
+
_tRotX=rx; _tRotY=ry; _tScl=s; _tPanX=panX; _tPanY=panY;
|
|
2321
2338
|
startKSpaceAnim();
|
|
2322
2339
|
}
|
|
2323
2340
|
}
|
|
@@ -2371,7 +2388,7 @@ if(systemThemeQuery){
|
|
|
2371
2388
|
═══════════════════════════════════════════════════════════════════════ */
|
|
2372
2389
|
var gl=null, glProgram=null, glBuf=null, glN=0;
|
|
2373
2390
|
var glAttribPos=-1, glAttribTime=-1;
|
|
2374
|
-
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;
|
|
2375
2392
|
var glU_halfRes=-1,glU_tMin=-1,glU_tMax=-1,glU_dot=-1,glU_color=-1;
|
|
2376
2393
|
|
|
2377
2394
|
// ── Cached bounds (computed once when data is uploaded) ──────────────
|
|
@@ -2389,16 +2406,16 @@ function initWebGL(){
|
|
|
2389
2406
|
gl.shaderSource(vs,'\
|
|
2390
2407
|
attribute vec3 aPos; attribute float aTime;\
|
|
2391
2408
|
uniform float uCy,uSy,uCx,uSx,uScale;\
|
|
2392
|
-
uniform
|
|
2409
|
+
uniform vec2 uPan; uniform vec2 uHalfRes;\
|
|
2393
2410
|
uniform float uTMin,uTMax,uDot;\
|
|
2394
2411
|
varying float vVis;\
|
|
2395
2412
|
void main(){\
|
|
2396
|
-
float dx=aPos.x
|
|
2413
|
+
float dx=aPos.x,dy=aPos.y,dz=aPos.z;\
|
|
2397
2414
|
float rx=dx*uCy-dz*uSy;\
|
|
2398
2415
|
float rz=dx*uSy+dz*uCy;\
|
|
2399
2416
|
float ry=dy*uCx-rz*uSx;\
|
|
2400
|
-
float sx=rx*uScale/uHalfRes.x;\
|
|
2401
|
-
float sy=ry*uScale/uHalfRes.y;\
|
|
2417
|
+
float sx=(rx*uScale+uPan.x)/uHalfRes.x;\
|
|
2418
|
+
float sy=(ry*uScale-uPan.y)/uHalfRes.y;\
|
|
2402
2419
|
vVis=(aTime>=uTMin&&aTime<=uTMax)?1.0:-1.0;\
|
|
2403
2420
|
gl_Position=vec4(sx,sy,0.0,1.0);\
|
|
2404
2421
|
gl_PointSize=vVis>0.0?uDot:0.0;\
|
|
@@ -2423,11 +2440,76 @@ function initWebGL(){
|
|
|
2423
2440
|
glAttribTime=gl.getAttribLocation(glProgram,"aTime");
|
|
2424
2441
|
glU_cy=gl.getUniformLocation(glProgram,"uCy");glU_sy=gl.getUniformLocation(glProgram,"uSy");
|
|
2425
2442
|
glU_cx=gl.getUniformLocation(glProgram,"uCx");glU_sx=gl.getUniformLocation(glProgram,"uSx");
|
|
2426
|
-
|
|
2443
|
+
glU_pan=gl.getUniformLocation(glProgram,"uPan");glU_scale=gl.getUniformLocation(glProgram,"uScale");
|
|
2427
2444
|
glU_halfRes=gl.getUniformLocation(glProgram,"uHalfRes");glU_dot=gl.getUniformLocation(glProgram,"uDot");
|
|
2428
2445
|
glU_tMin=gl.getUniformLocation(glProgram,"uTMin");glU_tMax=gl.getUniformLocation(glProgram,"uTMax");
|
|
2429
2446
|
glU_color=gl.getUniformLocation(glProgram,"uColor");
|
|
2430
|
-
return
|
|
2447
|
+
return !!(kDragging||_kTouchActive||_kAnimId);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/** Points to keep on screen while the camera is moving. */
|
|
2451
|
+
var KSPACE_MOVING_POINT_TARGET = 600000;
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* Stride to draw the cloud with while the camera is moving.
|
|
2455
|
+
*
|
|
2456
|
+
* Window culling cannot help when the whole sequence is in view: every point
|
|
2457
|
+
* really is visible, and 6.4 million of them onto a few hundred thousand
|
|
2458
|
+
* pixels costs about a second a frame. While the camera moves, every k-th
|
|
2459
|
+
* point is drawn instead.
|
|
2460
|
+
*
|
|
2461
|
+
* This is a true subset — each drawn point is a real ADC k-space location, and
|
|
2462
|
+
* nothing is interpolated or invented. The moment the drag or the easing
|
|
2463
|
+
* settles the full set is redrawn, so the image actually being read is always
|
|
2464
|
+
* exact; only the motion is cheaper. A subset also needs no extra memory: the
|
|
2465
|
+
* buffer is interleaved x,y,z,t at 16 bytes, so widening the attribute stride
|
|
2466
|
+
* to 16k selects every k-th vertex with no CPU work and no second upload.
|
|
2467
|
+
*
|
|
2468
|
+
* Returns 1 below the target, so narrow views and modest sequences are drawn
|
|
2469
|
+
* exactly even in motion.
|
|
2470
|
+
*/
|
|
2471
|
+
function kSpaceMovingStride(count){
|
|
2472
|
+
// The target is overridable so the settle-exactness guarantee can be tested
|
|
2473
|
+
// against a fixture small enough to ship; no repository fixture exceeds it.
|
|
2474
|
+
var target=(typeof window!=='undefined'&&window.__kMovingPointTarget>0)
|
|
2475
|
+
?window.__kMovingPointTarget:KSPACE_MOVING_POINT_TARGET;
|
|
2476
|
+
if(!(count>target))return 1;
|
|
2477
|
+
return Math.ceil(count/target);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/** True while a drag, touch gesture, or camera easing is in progress. */
|
|
2481
|
+
function kSpaceCameraMoving(){
|
|
2482
|
+
return !!(kDragging||_kTouchActive||_kAnimId);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* First index and count of the ADC points whose time lies in `[vs, ve]`.
|
|
2487
|
+
*
|
|
2488
|
+
* `kAdcTime` ascends by construction — src/pulseq/kspace.ts fills it block by
|
|
2489
|
+
* block in sequence order — and `uploadKSpaceGPU` writes the GPU buffer in that
|
|
2490
|
+
* same order, so the visible window is one contiguous run of vertices.
|
|
2491
|
+
*
|
|
2492
|
+
* Drawing only that run is not a reduction. The vertex shader already rejects
|
|
2493
|
+
* everything outside the window with `gl_PointSize = 0` and a `discard`, so the
|
|
2494
|
+
* pixels are identical; the GPU simply stops transforming millions of points in
|
|
2495
|
+
* order to throw them away. A 1.1 ms view of a 16 s sequence was submitting
|
|
2496
|
+
* 3.38 million vertices to draw about 218 of them.
|
|
2497
|
+
*
|
|
2498
|
+
* The range is padded by one point on each side and the shader keeps its own
|
|
2499
|
+
* time test, so an off-by-one here cannot change what is drawn.
|
|
2500
|
+
*
|
|
2501
|
+
* Pure and parameterised on purpose: the standalone app runs its renderer in an
|
|
2502
|
+
* IIFE with its own state and calls this global. Do not copy it into
|
|
2503
|
+
* web/index.html — a copy there would shadow this one and diverge.
|
|
2504
|
+
*/
|
|
2505
|
+
function kSpaceWindowRange(times,total,vs,ve){
|
|
2506
|
+
if(!times||!(total>0)||!(ve>=vs))return{first:0,count:0};
|
|
2507
|
+
var n=Math.min(total,times.length),lo=0,hi=n,mid;
|
|
2508
|
+
while(lo<hi){mid=(lo+hi)>>1;if(times[mid]<vs)lo=mid+1;else hi=mid;}
|
|
2509
|
+
var first=Math.max(0,lo-1);
|
|
2510
|
+
lo=first;hi=n;
|
|
2511
|
+
while(lo<hi){mid=(lo+hi)>>1;if(times[mid]<=ve)lo=mid+1;else hi=mid;}
|
|
2512
|
+
return{first:first,count:Math.max(0,Math.min(n,lo+1)-first)};
|
|
2431
2513
|
}
|
|
2432
2514
|
|
|
2433
2515
|
/* ── Upload ADC k‑space data to GPU (called after base64 decode) ────── */
|
|
@@ -2463,7 +2545,7 @@ document.getElementById("krst").onclick=function(){
|
|
|
2463
2545
|
kView="3d";
|
|
2464
2546
|
document.getElementById("kax").textContent="3D";
|
|
2465
2547
|
var af=_kAutoFitVals();
|
|
2466
|
-
setKSpaceTarget(-0.5, 0.7, af.scl, af.
|
|
2548
|
+
setKSpaceTarget(-0.5, 0.7, af.scl, af.panX, af.panY, false);
|
|
2467
2549
|
};
|
|
2468
2550
|
// Camera presets: smoothly rotate to look straight down an axis
|
|
2469
2551
|
document.getElementById("kax").onclick=function(){
|
|
@@ -2476,7 +2558,7 @@ document.getElementById("kax").onclick=function(){
|
|
|
2476
2558
|
else{trx=-0.5; tr=0.7;} // 3d — default perspective
|
|
2477
2559
|
document.getElementById("kax").textContent=kView.toUpperCase();
|
|
2478
2560
|
var af=_kAutoFitVals();
|
|
2479
|
-
setKSpaceTarget(trx, tr, af.scl, af.
|
|
2561
|
+
setKSpaceTarget(trx, tr, af.scl, af.panX, af.panY, false);
|
|
2480
2562
|
};
|
|
2481
2563
|
|
|
2482
2564
|
function resizeKc(){
|
|
@@ -2597,9 +2679,13 @@ kCanvas.addEventListener("wheel",function(e){e.preventDefault();
|
|
|
2597
2679
|
var r=kCanvas.getBoundingClientRect(),dpr=window.devicePixelRatio||1;
|
|
2598
2680
|
var mx=(e.clientX-r.left)/dpr, my=(e.clientY-r.top)/dpr;
|
|
2599
2681
|
var zf=e.deltaY<0?1.25:0.8, W=kCanvas.width/dpr, H=kCanvas.height/dpr;
|
|
2600
|
-
var dz=(1-1/zf)*dpr/kScl;
|
|
2601
2682
|
kAutoFit=false;
|
|
2602
|
-
|
|
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);
|
|
2603
2689
|
},{passive:false});
|
|
2604
2690
|
|
|
2605
2691
|
window.addEventListener("mousemove",function(e){
|
|
@@ -2612,18 +2698,25 @@ window.addEventListener("mousemove",function(e){
|
|
|
2612
2698
|
kRotY+=dx*0.008; kRotX-=dy*0.008;
|
|
2613
2699
|
_tRotY=kRotY; _tRotX=kRotX; // sync targets
|
|
2614
2700
|
}else{
|
|
2615
|
-
// right/middle drag = instant pan
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
if(Math.abs(cz)>0.01){kCy+=dy/cx;kCx+=(-dx-sz*sx*(dy/cx))/cz;}
|
|
2620
|
-
else{kCy+=dy/cx;kCz+=(dx-cz*sx*(dy/cx))/sz;}
|
|
2621
|
-
_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
|
|
2622
2705
|
kAutoFit=false;
|
|
2623
2706
|
}
|
|
2624
|
-
|
|
2707
|
+
scheduleKsDragDraw();
|
|
2708
|
+
});
|
|
2709
|
+
window.addEventListener("mouseup",function(){
|
|
2710
|
+
if(!kDragging)return;
|
|
2711
|
+
kDragging=false;kDragPrev=null;
|
|
2712
|
+
drawKsFast(); // settled: replace the moving subset with every point
|
|
2625
2713
|
});
|
|
2626
|
-
|
|
2714
|
+
|
|
2715
|
+
/** Draw at most once per frame, always from the latest rotation. */
|
|
2716
|
+
function scheduleKsDragDraw(){
|
|
2717
|
+
if(_kDragRaf)return;
|
|
2718
|
+
_kDragRaf=requestAnimationFrame(function(){_kDragRaf=0;drawKsFast();});
|
|
2719
|
+
}
|
|
2627
2720
|
|
|
2628
2721
|
/* ── Touch: 3D k-space viewer ─────────────────────────────────────── */
|
|
2629
2722
|
var _kTouchActive=false,_kTouchPrev=null,_kTouchPinch0=0,_kTouchMid=null,_kTouchBtn=0;
|
|
@@ -2663,18 +2756,15 @@ kCanvas.addEventListener("touchmove",function(e){
|
|
|
2663
2756
|
var mid=getTouchMid(e.touches);
|
|
2664
2757
|
var pdx=(mid.x-_kTouchPrev.x), pdy=(mid.y-_kTouchPrev.y);
|
|
2665
2758
|
_kTouchPrev={x:mid.x,y:mid.y};
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
pdx/=(dpr*kScl); pdy/=(dpr*kScl);
|
|
2669
|
-
if(Math.abs(cz)>0.01){kCy+=pdy/cx;kCx+=(-pdx-sz*sx*(pdy/cx))/cz;}
|
|
2670
|
-
else{kCy+=pdy/cx;kCz+=(pdx-cz*sx*(pdy/cx))/sz;}
|
|
2671
|
-
_tCy=kCy; _tCx=kCx; _tCz=kCz; kAutoFit=false;
|
|
2759
|
+
kPanX+=pdx; kPanY+=pdy;
|
|
2760
|
+
_tPanX=kPanX; _tPanY=kPanY; kAutoFit=false;
|
|
2672
2761
|
drawKsFast();
|
|
2673
2762
|
}
|
|
2674
2763
|
e.preventDefault();
|
|
2675
2764
|
},{passive:false});
|
|
2676
2765
|
kCanvas.addEventListener("touchend",function(e){
|
|
2677
2766
|
_kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null;
|
|
2767
|
+
if(!e.touches.length)drawKsFast();
|
|
2678
2768
|
if(e.touches.length===1){
|
|
2679
2769
|
// Transition from 2‑finger to 1‑finger
|
|
2680
2770
|
_kTouchActive=true;_kTouchBtn=0;
|
|
@@ -2683,6 +2773,7 @@ kCanvas.addEventListener("touchend",function(e){
|
|
|
2683
2773
|
});
|
|
2684
2774
|
kCanvas.addEventListener("touchcancel",function(){
|
|
2685
2775
|
_kTouchActive=false;_kTouchPrev=null;_kTouchPinch0=0;_kTouchMid=null;
|
|
2776
|
+
drawKsFast();
|
|
2686
2777
|
});
|
|
2687
2778
|
|
|
2688
2779
|
/* ── Resize handle ───────────────────────────────────────────────────── */
|
|
@@ -2770,8 +2861,13 @@ function persistPanelSize(){
|
|
|
2770
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;}
|
|
2771
2862
|
|
|
2772
2863
|
function _kAutoFitVals(){
|
|
2773
|
-
var
|
|
2774
|
-
|
|
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)};
|
|
2775
2871
|
}
|
|
2776
2872
|
|
|
2777
2873
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
@@ -2842,7 +2938,7 @@ function drawKs_core(W,H,dpr){
|
|
|
2842
2938
|
if(kAutoFit){
|
|
2843
2939
|
kAutoFit=false;
|
|
2844
2940
|
var af=_kAutoFitVals();
|
|
2845
|
-
setKSpaceTarget(kRotX, kRotY, af.scl, af.
|
|
2941
|
+
setKSpaceTarget(kRotX, kRotY, af.scl, af.panX, af.panY, false);
|
|
2846
2942
|
}
|
|
2847
2943
|
|
|
2848
2944
|
// ── Time window ──
|
|
@@ -2859,7 +2955,7 @@ function drawKs_core(W,H,dpr){
|
|
|
2859
2955
|
|
|
2860
2956
|
gl.uniform1f(glU_cy,Math.cos(kRotY));gl.uniform1f(glU_sy,Math.sin(kRotY));
|
|
2861
2957
|
gl.uniform1f(glU_cx,Math.cos(kRotX));gl.uniform1f(glU_sx,Math.sin(kRotX));
|
|
2862
|
-
gl.
|
|
2958
|
+
gl.uniform2f(glU_pan,kPanX*dpr,kPanY*dpr);
|
|
2863
2959
|
gl.uniform1f(glU_scale,kScl);
|
|
2864
2960
|
gl.uniform2f(glU_halfRes,hW,hH);
|
|
2865
2961
|
gl.uniform1f(glU_tMin,vs);gl.uniform1f(glU_tMax,ve);
|
|
@@ -2869,14 +2965,19 @@ function drawKs_core(W,H,dpr){
|
|
|
2869
2965
|
var rgb=parseCSSColor(ac);
|
|
2870
2966
|
gl.uniform4f(glU_color,rgb[0],rgb[1],rgb[2],0.85);
|
|
2871
2967
|
|
|
2968
|
+
var win=kSpaceWindowRange(kAdcTime,glN,vs,ve);
|
|
2969
|
+
var stride=kSpaceCameraMoving()?kSpaceMovingStride(win.count):1;
|
|
2970
|
+
|
|
2872
2971
|
gl.bindBuffer(gl.ARRAY_BUFFER,glBuf);
|
|
2873
2972
|
gl.enableVertexAttribArray(glAttribPos);
|
|
2874
|
-
gl.vertexAttribPointer(glAttribPos,3,gl.FLOAT,false,16,0);
|
|
2973
|
+
gl.vertexAttribPointer(glAttribPos,3,gl.FLOAT,false,16*stride,0);
|
|
2875
2974
|
gl.enableVertexAttribArray(glAttribTime);
|
|
2876
|
-
gl.vertexAttribPointer(glAttribTime,1,gl.FLOAT,false,16,12);
|
|
2975
|
+
gl.vertexAttribPointer(glAttribTime,1,gl.FLOAT,false,16*stride,12);
|
|
2877
2976
|
|
|
2878
2977
|
kSpaceTrajectoryDrawCount++;
|
|
2879
|
-
|
|
2978
|
+
// Stride reindexes the buffer, so the window offset and count scale with it.
|
|
2979
|
+
var drawFirst=Math.floor(win.first/stride),drawCount=Math.floor(win.count/stride);
|
|
2980
|
+
if(drawCount>0)gl.drawArrays(gl.POINTS,drawFirst,drawCount);
|
|
2880
2981
|
}
|
|
2881
2982
|
|
|
2882
2983
|
drawKsOverlay(W,H,dpr,cs);
|
|
@@ -2900,11 +3001,10 @@ function drawKsOverlay(W,H,dpr,cs){
|
|
|
2900
3001
|
var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cxR=Math.cos(kRotX),sxR=Math.sin(kRotX);
|
|
2901
3002
|
var invDpr=1/dpr;
|
|
2902
3003
|
function proj(px,py,pz){
|
|
2903
|
-
var
|
|
2904
|
-
var
|
|
2905
|
-
var
|
|
2906
|
-
|
|
2907
|
-
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};
|
|
2908
3008
|
}
|
|
2909
3009
|
var tick=kNice(rng3);
|
|
2910
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
|