seqeyes-python 0.3.5__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.
Files changed (20) hide show
  1. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/PKG-INFO +1 -1
  2. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/pyproject.toml +1 -1
  3. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/__init__.py +1 -1
  4. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/resources/pulseq-bundle.js +1 -1
  5. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/resources/webview-bundle.js +51 -41
  6. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/.gitignore +0 -0
  7. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/README.md +0 -0
  8. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/examples/demo.ipynb +0 -0
  9. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/examples/demo.py +0 -0
  10. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/_plot.py +0 -0
  11. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/_renderer.py +0 -0
  12. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/src/seqeyes/resources/viewer.html +0 -0
  13. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/conftest.py +0 -0
  14. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/debug_full.html +0 -0
  15. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/debug_minimal.html +0 -0
  16. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/debug_viewer.html +0 -0
  17. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/debug_viewer.py +0 -0
  18. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/integration_test.py +0 -0
  19. {seqeyes_python-0.3.5 → seqeyes_python-0.3.6}/tests/test_plot.py +0 -0
  20. {seqeyes_python-0.3.5 → 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.5
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.5"
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"}
@@ -29,4 +29,4 @@ from seqeyes._plot import set, reset
29
29
  from seqeyes._renderer import SeqEyesViewer
30
30
 
31
31
  __all__ = ["set", "reset", "SeqEyesViewer"]
32
- __version__ = "0.3.5"
32
+ __version__ = "0.3.6"
@@ -97,7 +97,7 @@ var Pulseq = (() => {
97
97
  });
98
98
 
99
99
  // package.json
100
- var version = "0.3.5";
100
+ var version = "0.3.6";
101
101
 
102
102
  // src/pulseq/decompressor.ts
103
103
  function decompressShape(compressed, numSamples) {
@@ -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
- var kCx=0, kCy=0, kCz=0, kScl=1; // view center & zoom
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, _tCx=kCx, _tCy=kCy, _tCz=kCz;
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(_tCx - kCx) > 0.001){ kCx += (_tCx - kCx) * _kEasing; changed = true; }
2308
- if (Math.abs(_tCy - kCy) > 0.001){ kCy += (_tCy - kCy) * _kEasing; changed = true; }
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; kCx=_tCx; kCy=_tCy; kCz=_tCz;
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, cx, cy, cz, instant) {
2332
+ function setKSpaceTarget(rx, ry, s, panX, panY, instant) {
2324
2333
  if (instant) {
2325
- kRotX=_tRotX=rx; kRotY=_tRotY=ry; kScl=_tScl=s; kCx=_tCx=cx; kCy=_tCy=cy; kCz=_tCz=cz;
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; _tCx=cx; _tCy=cy; _tCz=cz;
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,glU_center=-1,glU_scale=-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 vec3 uCenter; uniform vec2 uHalfRes;\
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-uCenter.x,dy=aPos.y-uCenter.y,dz=aPos.z-uCenter.z;\
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
- glU_center=gl.getUniformLocation(glProgram,"uCenter");glU_scale=gl.getUniformLocation(glProgram,"uScale");
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.cx, af.cy, af.cz, false);
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.cx, af.cy, af.cz, false);
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
- setKSpaceTarget(kRotX, kRotY, kScl*zf, kCx+(mx-W/2)*dz, kCy-(my-H/2)*dz, kCz, false);
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
- var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX);
2690
- var dpr=window.devicePixelRatio||1;
2691
- dx/=(dpr*kScl); dy/=(dpr*kScl);
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
- var cz=Math.cos(kRotY),sz=Math.sin(kRotY),cx=Math.cos(kRotX),sx=Math.sin(kRotX);
2750
- var dpr=window.devicePixelRatio||1;
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 rng=_kBrng||1,W=kCanvas.width/(window.devicePixelRatio||1),H=kCanvas.height/(window.devicePixelRatio||1);
2859
- return{cx:(_kBxmin+_kBxmax)/2,cy:(_kBymin+_kBymax)/2,cz:(_kBzmin+_kBzmax)/2,scl:Math.min(W,H)/(rng*1.15)};
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.cx, af.cy, af.cz, false);
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.uniform3f(glU_center,kCx,kCy,kCz);
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 dx2=px-kCx, dy2=py-kCy, dz2=pz-kCz;
2994
- var rx=dx2*cz-dz2*sz;
2995
- var rz2=dx2*sz+dz2*cz;
2996
- var ry=dy2*cxR-rz2*sxR;
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